Práctica 2: Ejercicio 1 - Preprocesamiento y análisis descriptivo de datos¶
Asignatura: Machine Learning, 2025/2026
Profesor: José Tomás Palma Méndez
Máster en Inteligencia Artificial
Autores:
- Jesús Guirado Pérez
- Antonio Luis Sánchez Torres
- Víctor Emilio Vicente García
Introducción¶
En el siguiente problema, disponemos de un conjunto de datos que contiene datos relacionados con el consumo de energía y datos climáticos de un barrio de una determinada ciudad entre los años 2014 y 2022. Estos datos representan series temporales que reflejan la evolución de cada variable a lo largo del período temporal mencionado. Se nos pide que se desarrollemos modelos predictivos univariantes y multivariantes, teniendo en cuenta distintos conjuntos de las variables aportadas, para la predicción en un horizonte temporal de 7 días de la energía total consumida en dicho barrio. Para ello, primero realizaremos un preprocesamiento y un análisis descriptivo tanto de los datos de consumo y producción energéticos como de los datos climáticos del barrio en cuestión. Después, tras preprocesar los datos, entrenaremos distintos modelos univariantes usando sólo la variable objetivo, para intentar ver cuál es el que mejor se comporta a la hora de predecir los datos de la semana siguiente. A continuación, haremos lo propio pero con modelos multivariantes en los que tendremos en cuenta el resto de variables energéticas. Por último, haremos de nuevo lo mismo pero entrenando los modelos usando tanto las variables climáticas como las energéticas. Intentaremos establecer cuál es el mejor modelo a usar en cada uno de estos apartados.
Para que sea más fácil de programar y ejecutar, hemos separado cada uno de estos apartados en 4 notebooks distintos. Es este, realizaremos el análisis y preprocesamiento de los datos.
Resumen ejecutivo¶
En cuanto al preprocesamiento de datos, primero tratamos el dataset Datos_Energia. Para este dataset, primero creamos un DatetimeIndex a partir de las columnas Año, Mes, Día y Hora. Una vez creado, eliminamos estas cuatro variables, quedándonos con siete columnas en el dataset. Tras esto, revisamos si existen fechas perdidas, que en este caso no hay. En cuanto al tratamiento de duplicados, al ser pocos, decidimos no modificarlos. Respecto a los valores nulos, los tratamos mediante interpolación temporal, ya que este método tiene en cuenta las fechas reales del índice temporal y funciona al igual que la interpolación lineal. La variable Día de la semana no se modifica en todo el análisis, ya que a conluimos que es una variable exógena.
En relación con el tratamiento de outliers, observamos que algunas variables presentaban valores muy altos o muy bajos que desequilibran las series temporales, afectando a sus medias y desviaciones típicas. Además, existen valores negativos que no tienen sentido en este contexto, por lo que los consideramos outliers y los tratamos, mediante percentiles, como valores nulos para poder procesarlos posteriormente mediante interpolación temporal. No obstante, existe un pequeño matiz en las variables Electricidad (kW), Refrigeración (kW), Calefacción (kWh) y Energía total (kWh), ya que la variable objetivo, Energía total (kWh), es la suma de las otras tres. Por ello, decidimos que cada una de estas variables calcule primero sus valores faltantes a partir de las otras tres, en caso de que tenga nulos. Los nulos restantes se tratan posteriormente mediante interpolación temporal. En el caso de Energía total (kWh), todos sus nulos se tratan utilizando las otras tres variables, ya que estas ya no contienen ni outliers ni valores nulos.
Finalmente, analizamos la varianza, la correlación de cada variable con la variable objetivo y la matriz de correlación. En este caso no eliminamos ninguna variable, ya que el dataset contiene pocas variables y no queríamos limitar el análisis, pese al riesgo de multicolinealidad. Después, pasamos el dataset de frecuencia horaria a frecuencia diaria, aplicando la suma a todas las variables, excepto Día de la semana, que además de la suma dividimos entre 24 y lo pasamos a entero. Para finalizar, declaramos como variables endógenas a Electricidad (kW), Fotovoltaica (kW), Refrigeración (kW), Calefacción (kWh) y Emisión (kg CO₂), ya que influyen directamente en la variable objetivo, dejando como variable exógena Día de la semana. Guardamos el dataset con el nombre energia_preprocesada para poder ser utilizado en los siguientes ejercicios.
En cuanto al dataset de Datos_Climaticos, seguimos unos pasos muy similares. Primero creamos el DatetimeIndex a partir de las variables Año, Mes, Día y Hora. Eliminamos estas cuatro variables, quedándonos con diez columnas en el dataset. Día de la semana la tratamos igualmente como variable exógena. En cuanto a los duplicados, al ser tan pocos decidimos no modificarlos, ya que, de 78.888 datos, que existan duplicados no es algo extraño. El tratamiento de nulos se realiza mediante interpolación temporal para las variables numéricas, y para las variables object utilizamos primero ffill, rellenando los nulos con el valor anterior, y posteriormente bfill en caso de que existan más nulos consecutivos, rellenándolos con el valor posterior. A continuación, tratamos los outliers de las variables Temperatura (°C) y Presión (hPa) a través de percentiles, transformándolos en valores nulos para posteriormente aplicar interpolación temporal.
Después, transformamos las variables object, Dirección del viento y Condición meteorológica, en variables booleanas mediante dummies, eliminando la primera columna de cada una de ellas, ya que realmente no aportan información relevante a los modelos. A continuación, unimos el dataset de energía ya preprocesado anteriormente (antes de pasarlo a frecuencia diaria) con el preprocesado del dataset climático, eliminando la variable repetida, lo que nos deja con 47 columnas en el dataset. A partir de aquí, analizamos la varianza, observando que existen 34 variables con valores próximos a cero, los cuales eliminamos quedándonos finalmente con 13 variables. Al analizar la correlación con la variable objetivo no eliminamos ninguna variable, pero al estudiar la matriz de correlación eliminamos Emisión (kg CO₂), ya que, con 13 variables, podemos prescindir de ella (puesto que consideramos que ya tenemos suficientes) y así reducir el riesgo de multicolinealidad, haciéndonos quedar al final con 12 variables.
Posteriormente, transformamos los datos de frecuencia horaria a diaria, agregando los datos aplicando la suma a Día de la semana y al resto de variables energéticas, la media diaria a las variables climáticas y el máximo diario a la variable Ráfaga de viento (m/s). Declaramos como variables endógenas: Electricidad (kW), Fotovoltaica (kW), Refrigeración (kW) y Calefacción (kWh); y como variables exógenas: Día de la semana, Temperatura (°C), Punto de rocío (°C), Humedad (%), Presión (hPa), Velocidad del viento (m/s) y Ráfaga de viento (m/s). Por último, guardamos los datos preprocesados de energía y clima en un único dataset llamado energia_climaticos_preprocesada.
En cuanto al análisis descriptivo, observamos que todas las variables analizadas en los datasets energia_preprocesada y energia_climaticos_preprocesada, excepto Día de la semana, ya que esta última no la analizamos, presentan estacionalidad anual, por lo que es esperable que estos patrones se repitan en el futuro. Además, Energía total (kWh) presenta también estacionalidad semanal. Las variables son estacionarias, por lo que la media y la varianza se mantienen constantes y la estructura temporal no cambia, haciendo innecesaria la aplicación de transformaciones adicionales. Ninguna variable sigue una distribución normal. Todas las variables endógenas presentan una alta autocorrelación con lag = 7, lo que indica una fuerte dependencia entre valores presentes y pasados. La tendencia, el ruido y los boxplots varían en función de cada variable.
Para hacer la construcción de modelos univariantes, utilizamos el dataset preprocesado que contiene únicamente los datos energéticos. Hacemos la división temporal en proporción 80/20 (train/test) y la validación se realiza con TimeSeriesFold (usando 2 años de entrenamiento inicial y ventana deslizante anual), manteniendo un horizonte de 7 días. La selección de lags se guía por el análisis de estacionalidad: se prueban varias estrategias, X días anteriores siendo X=[7, 30, 183, 365] y otra que combina los últimos 7 días y la misma semana del año anterior [1–7, 359–365], capturando dependencia semanal y anual conjuntamente con menos lags.
Creamos un modelo baseline como referencia, que usa una media entre el último día conocido o la última predicción y el mismo día del año anterior, que pretende capturar tendencia reciente y estacionalidad anual. Este baseline actúa como referencia mínima para el rendimiento del resto de modelos y que esperamos mejorar al entrenar el resto de modelos más complejos.
Como principales modelos univariantes usamos tres: XGBoost, RandomForest y SVR. Optimizamos cada uno mediante búsqueda con grid de hiperparámetros. Para escoger los valores del grid, escogemos los predeterminados y elegimos uno ligeramente inferior y superior (o 2 superiores, cuando escoger inferiores no es posible). El modelo SVR es el que mejor métricas obtiene de todos con un MAE de 98652.42, un RMSE de 126039.10, un RMSSE de 0.7151 y un MAPE del 8.29%. Para elegir el mejor, nos fijamos sobre todo en el RMSSE y el MAPE. Tanto en train como en test, los modelos univariantes mejoran los resultados del baseline de forma consistente y lo superan en todos los horizontes de predicción (h+1 a h+7), mostrando una degradación del error más controlada que el modelo base conforme avanza el horizonte.
Los tres modelos entrenados escogen como mejor ventana temporal un valor de lag de 30. Además, al calcular la importancia de las variables utilizadas (en los modelos que lo permiten), vemos que los días más importantes para hacer la predicción del siguiente son los del lag 1, el lag 6 y el lag 7. Esto concuerda mucho con el hecho de que la serie presenta estacionalidad semanal.
En resumen, para esta serie con estacionalidad anual destacada, los modelos univariantes superan de forma clara al modelo de línea base en el horizonte de 7 días, siendo el SVR el enfoque más robusto y preciso. Esto indica que, con las configuraciones empleadas, la combinación de estructura temporal y capacidad de modelado no lineal permite capturar mejor la dinámica de la serie que la estrategia heurística del baseline.
Para la predicción multivariante de Energía total (kWh) se parte del conjunto diario preprocesado, eliminando Emisión (kg CO₂) por redundancia con la variable objetivo y reincorporando Día de la semana como variable exógena. La división temporal es 80/20 (train/test) y la validación se realiza con TimeSeriesFold (3 años de entrenamiento inicial y ventana deslizante anual), manteniendo un horizonte de 7 días. La selección de lags se guía por el análisis de estacionalidad: se prueban varias estrategias, X días anteriores siendo X=[7, 30, 183, 365] y otra que combina los últimos 7 días y la misma semana del año anterior [1–7, 359–365], capturando dependencia semanal y anual conjuntamente con menos lags.
Como línea base se usa una media entre el último día conocido o la última predicción y el mismo día del año anterior, que pretende capturar tendencia reciente y estacionalidad anual. Este baseline actúa como referencia mínima de desempeño y resulta ser el peor modelo en comparación con los enfoques multivariantes, tanto en métricas globales como por horizonte de predicción.
Sobre modelos multivariantes, se entrena predicción directa con Random Forest, XGBoost y SVR, realizando grid search de hiperparámetros junto a la búsqueda de lags. En el análisis multivariante sin variables climáticas, el modelo XGBOOST destaca como el mejor, alcanzando un MAPE de 7.67%, superando claramente al baseline y al resto de modelos. Tanto en train como en test, los modelos multivariantes baten al baseline de forma consistente y lo mejoran en todos los horizontes de predicción (h+1 a h+7).
El análisis de importancia de características refuerza las conclusiones del descriptivo: los lags de la propia serie son los predictores dominantes, con especial peso de lag_1 (inercia diaria) el lag_1 de la variable de Refrigeración tambien destaca; En cuanto a la configuración de lags, todos los modelos consiguen el mejor resultado apartir de los últimos 7 días.
En conjunto, para esta serie con estacionalidad marcada, los modelos multivariantes superan de forma clara al baseline en el horizonte de 7 días, siendo el XGBOOST el enfoque más robusto y preciso. Esto indica que, con las configuraciones empleadas, la combinación de estructura temporal, permite capturar mejor la dinámica de la serie que una estrategia puramente heurística como el modelo de linea base.
En el análisis multivariante con variables climáticas, partimos del dataset integrado y preprocesado. Se incorporan variables exógenas meteorológicas (temperatura media, humedad, ráfagas de viento, etc.) junto con día de la semana. El objetivo es capturar tanto la estacionalidad semanal/anual de la demanda como su sensibilidad a las condiciones climáticas, variables que pueden predecirse con antelación e incorporarse como exógenas conocidas.
Se mantiene la misma estrategia de evaluación que en el análisis multivariante sin clima, utilizando los mismos lags, modelos e hiperparámetros.
En este escenario, el modelo SVR es el que obtiene el mejor desempeño, con un MAPE de 5.14% y un RMSSE de 0.4875, superando al baseline, que alcanza un error MAPE de 9,28% y RMSSE de 0.8538. Al igual que en el caso anterior, los modelos con variables climáticas mejoran al modelo base en todos los horizontes de predicción, confirmando que la incorporación de información meteorológica aporta valor adicional y estabilidad a medio plazo. Destacan tambien la estabilidad de las predicciónes conforme avanza el horizonte, siendo más estables al incluir los datos climáticos. El modelo SVR con datos climáticos ha sido con diferencia el mejor modelo entrenado. Las variables climáticas han conseguido mejorar los resultados de los 3 modelos principales, por lo que indican que son importantes para la predicción.
Análisis descriptivo de datos y preprocesamiento¶
Librerías¶
Primero, cargamos las librerías que utilizaremos a lo largo del ejercicio.
# Procesamiento de datos
# ==============================================================================
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
import matplotlib.dates as mdates
from statsmodels.tsa.seasonal import seasonal_decompose
Lectura de datasets¶
Cargamos los datasets con los que trabajaremos en la práctica, Datos_Energia.csv y Datos_Climaticos.csv
# Cargamos nuestros dos datasets
energia = pd.read_csv("Data/Datos_Energia.csv")
climaticos = pd.read_csv("Data/Datos_Climaticos.csv")
El dataframe energia contendrá el dataset Datos_Energia.csv y el dataframe climáticos, el dataset Datos_Climaticos.csv
Análisis superficial de los datasets¶
Procedemos ahora a hacer un primer análisis los dos datasets que trataremos en la práctica.
Observamos primero el dataset con los datos sobre el consumo de energía.
# Información del dataset
energia.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 78888 entries, 0 to 78887 Data columns (total 11 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Año 78888 non-null int64 1 Mes 78888 non-null int64 2 Día 78888 non-null int64 3 Hora 78888 non-null int64 4 Día de la semana 78888 non-null int64 5 Electricidad (kW) 78871 non-null float64 6 Fotovoltaica (kW) 78871 non-null float64 7 Refrigeración (kW) 78871 non-null float64 8 Calefacción (kWh) 78871 non-null float64 9 Energía total (kWh) 78871 non-null float64 10 Emisión (kg CO₂) 78871 non-null float64 dtypes: float64(6), int64(5) memory usage: 6.6 MB
Aquí podemos ver que el primer dataset tiene 11 variables, de las cuales, las 4 primeras variables contienen datos que podemos utilizar para crear el índice temporal ("DatetimeIndex") para el mismo. También podemos ver que hay 78888 entradas. Además, todas las variables son numéricas, por lo que no hará falta realizar convertir ninguna en dummy o usar codificación one-hot. Por último, podemos ver que deberemos hacer también un tratamiento de valores perdidos (missing), ya que hay 6 columnas (Electricidad (kW), Fotovoltaica (kW), Refrigeración (kW), Calefacción (kWh), Energía total (kWh) y Emisión (kg CO₂)) con 17 valores nulos.
Procedemos ahora a observar el dataset con los datos climáticos.
# Información del dataset
climaticos.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 78888 entries, 0 to 78887 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Año 78888 non-null int64 1 Mes 78888 non-null int64 2 Día 78888 non-null int64 3 Hora 78888 non-null int64 4 Día de la semana 78888 non-null int64 5 Temperatura (°C) 78701 non-null float64 6 Punto de rocío (°C) 78701 non-null float64 7 Humedad (%) 78701 non-null float64 8 Dirección del viento 78355 non-null object 9 Velocidad del viento (m/s) 78701 non-null float64 10 Ráfaga de viento (m/s) 78701 non-null float64 11 Presión (hPa) 78701 non-null float64 12 Precipitación (mm) 78701 non-null float64 13 Condición meteorológica 78695 non-null object dtypes: float64(7), int64(5), object(2) memory usage: 8.4+ MB
En este dataset podemos ver que tenemos 14 variables, de las cuales, las 4 primeras columnas se utilizarán para realizar un índice temporal ( "DatetimeIndex") para el mismo. Podemos ver cómo también tenemos 78888 datos, donde tenemos 2 variables "object", (Dirección del viento y Condición meteorológica), que trataremos con variables dummies. Por último, todas las variables excepto las 5 primeras contienen valores nulos, por lo que también tendremos que tratarlos en este dataset.
Creación del índice temporal¶
Vamos a crear nuestro índice temporal (DatetimeIndex) que nos será necesario para nuestra práctica de series temporales.
Lo haremos siguiendo la estructura de "Año-Mes-Dia Hora:Minuto:Segundo". Para ello, procesaremos las variables Año, Mes, Dia y Hora como strings siguiendo la estructura de antes, además de añadir al final por nosotros mismos los minutos y segundos como ":00:00".
Luego, procedemos a eliminar estas variables, ya que no nos serán necesarias una vez que creamos nuestro índice.
# Creamos un datetime con el año, mes, dia y hora (minutos y segundos lo ponemos a 00 los dos)
energia.index = pd.to_datetime(energia['Año'].astype(str) + '-' + energia['Mes'].astype(str) + '-' + energia['Día'].astype(str) + ' ' + energia['Hora'].astype(str) + ':00:00')
climaticos.index = pd.to_datetime(climaticos['Año'].astype(str) + '-' + climaticos['Mes'].astype(str) + '-' + climaticos['Día'].astype(str) + ' ' + climaticos['Hora'].astype(str) + ':00:00')
# Eliminamos las columnas que hemos utilizado para crear el datetime
energia = energia.drop(columns=['Año', 'Mes', 'Día', 'Hora'])
climaticos = climaticos.drop(columns=['Año', 'Mes', 'Día', 'Hora'])
Comprobamos que, efectivamente, hemos hecho el índice de forma correcta y que hemos eliminado las variables.
# Información del dataset
energia.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 78888 entries, 2014-01-01 00:00:00 to 2022-12-31 23:00:00 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Día de la semana 78888 non-null int64 1 Electricidad (kW) 78871 non-null float64 2 Fotovoltaica (kW) 78871 non-null float64 3 Refrigeración (kW) 78871 non-null float64 4 Calefacción (kWh) 78871 non-null float64 5 Energía total (kWh) 78871 non-null float64 6 Emisión (kg CO₂) 78871 non-null float64 dtypes: float64(6), int64(1) memory usage: 4.8 MB
Vemos que el dataset de energía, con 7 variables en total, ya no tiene esas 4 variables que hemos utilizado para crear el DatetimeIndex. Este tiene 78888 entradas, de la fecha 2014-01-01 00:00:00 a la fecha 2022-12-31 23:00:00. Debemos tener en cuenta que las variables Electricidad (kW), Fotovoltaica (kW), Refrigeración (kW), Calefacción (kWh), Energía total (kWh) y Emisión (kg CO₂) tienen 17 nulos cada una.
# Información del dataset
climaticos.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 78888 entries, 2014-01-01 00:00:00 to 2022-12-31 23:00:00 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Día de la semana 78888 non-null int64 1 Temperatura (°C) 78701 non-null float64 2 Punto de rocío (°C) 78701 non-null float64 3 Humedad (%) 78701 non-null float64 4 Dirección del viento 78355 non-null object 5 Velocidad del viento (m/s) 78701 non-null float64 6 Ráfaga de viento (m/s) 78701 non-null float64 7 Presión (hPa) 78701 non-null float64 8 Precipitación (mm) 78701 non-null float64 9 Condición meteorológica 78695 non-null object dtypes: float64(7), int64(1), object(2) memory usage: 6.6+ MB
Como resultado, en el dataset de climáticos podemos ver cómo nos hemos quedado con 10 variables, habiendo eliminado las primeras 4 variables que hemos utilizado para crear el DatetimeIndex. Este tiene 78888 entradas, de la fecha 2014-01-01 00:00:00 a la fecha 2022-12-31 23:00:00. Debemos tener en cuenta que las variables Temperatura (°C), Punto de rocío (°C), Humedad (%), Velocidad del viento (m/s), Ráfaga de viento (m/s), Presión (hPa) y Precipitación (mm) tienen 187 nulos cada una. Por su parte, Condición meteorológica tiene 193 nulos y Dirección del viento, 533 nulos.
Por lo tanto, ya tenemos nuestros dos datasets con sus índices temporales (DatetimeIndex) de frecuencia horaria.
Datos de Energía¶
Una vez hecho un análisis general de los datasets, además de crear su índice temporal, ahora vamos a centrarnos en el dataset de Datos_Energia.csv.
Fechas perdidas¶
Comprobamos si tenemos fechas perdidas. Lo haremos comprobando la frecuencia horaria, que es como manipularemos el dataset de momento.
# Se calcula el numero de fechas desde a través de las hora
fechas_esperadas = pd.date_range(start=energia.index.min(), end=energia.index.max(), freq='h')
# Diferencias entre la cantidad de horas que se supone que tenemos, y lo que realmente tenemos
fechas_perdidas = fechas_esperadas.difference(energia.index)
# Imprimimos el número de horas perdidas
print(f'Número de fechas faltantes: {len(fechas_perdidas)}')
# Mostramos las horas pérdidas
fechas_perdidas
Número de fechas faltantes: 0
DatetimeIndex([], dtype='datetime64[ns]', freq='h')
Vemos que no tenemos fechas perdidas, por lo que están colocadas todas las horas de cada día en el índice.
Tratamiento de duplicados¶
Ahora analizaremos si tenemos valores duplicados en nuestro conjunto de datos.
# Visualizamos la cantidad de duplicados del dataset
energia.duplicated().sum()
np.int64(63)
Observamos que tenemos 63 valores duplicados, pero decidimos no eliminarlos porque, del total de 78888 valores que contiene nuestro dataset, consideramos que es factible que puedan existir valores duplicados a lo largo de todas las horas de todos los años incluidos. Por tanto, consideramos que no son valores erróneos y no los eliminamos.
energia.index.duplicated().sum()
np.int64(0)
Vemos cómo no tenemos índices duplicados, por lo que no tenemos dos días con índices repetidos.
Tratamiento de nulos¶
Vamos a comprobar los nulos que tenemos ahora.
# Visualizamos la cantidad de nulos que tiene el dataset
energia.isna().sum()
Día de la semana 0 Electricidad (kW) 17 Fotovoltaica (kW) 17 Refrigeración (kW) 17 Calefacción (kWh) 17 Energía total (kWh) 17 Emisión (kg CO₂) 17 dtype: int64
Como se puede ver hay pocos nulos (17), por lo que podríamos eliminarlos o imputarlos, en nuestro caso, como no queremos eliminar ya que no queremos perder datos, decidimos imputar.
Imputación mediante interpolación temporal¶
En primer lugar, para las variables Electricidad (kW), Refrigeración (kW), Calefacción (kWh) y Energía total (kWh), utilizaremos la relación entre estas 4 variables para calcularlas manualmente. Siempre que tratemos los nulos de una de estas variables, y las otras tres variables no tengan nulos en esa misma fila, calcularamos la misma en base a las otras tres. Si no, no se hará nada y se tratará con interpolación. La excepción es Energía total (kWh), ya que lo que hacemos en esta variable es sustituir los datos que no coinciden con la suma de las otras 3 variables, ya que así podremos tratar los outliers también directamente. La imputación de valores nulos la haremos más adelante, cuando tratemos los outliers, puesto que estos los sustituiremos también por nulos
En nuestro dataset, nuestros datos están organizados por horas, por lo que pensamos sacar ventaja sobre ello. Por lo tanto, "method = time" utiliza el DatetimeIndex para estimar los valores faltantes teniendo en cuenta la distancia real entre timestamps (en este caso, horas). Viendo la documentación, vemos que funciona en datos diarios o con mayor resolución, por lo que es adecuado para series horarias como esta. Además, al haber pocos valores nulos, la interpolación temporal proporciona una aproximación suave y coherente sin distorsionar el comportamiento general de la serie.
Tratamiento de outliers y valores nulos¶
Vamos a analizar si hay valores sospechosos en cada una de las variables. Primero, analizaremos los 5 primeros datos de nuestro dataset.
# Visualizamos las 5 primeras columnas del dataset
energia.head()
| Día de la semana | Electricidad (kW) | Fotovoltaica (kW) | Refrigeración (kW) | Calefacción (kWh) | Energía total (kWh) | Emisión (kg CO₂) | |
|---|---|---|---|---|---|---|---|
| 2014-01-01 00:00:00 | 2 | 21294.33 | 2.39 | 8098.10352 | 876.28229 | 30268.37288 | 18880.0 |
| 2014-01-01 01:00:00 | 2 | 21190.46 | 1.95 | 7312.22987 | 873.35158 | 29377.43704 | 18320.0 |
| 2014-01-01 02:00:00 | 2 | 21537.07 | 4.29 | 7305.86410 | 911.45081 | 29752.56792 | 18560.0 |
| 2014-01-01 03:00:00 | 2 | 21688.66 | 9.65 | 7573.82433 | 870.42087 | 30130.62951 | 18790.0 |
| 2014-01-01 04:00:00 | 2 | 23044.40 | 10.19 | 7462.96849 | 946.61933 | 31455.31043 | 19620.0 |
De momento, en las 5 primeras filas no vemos nada raro. Vamos a revisar en más profundidad haciendo un análisis visual de las series temporales.
# Guarda el número de variables del dataset
n = len(energia.columns)
# Creamos las figuras y ejes con 1 columna que cada fila tendrá una variable, en figsize poniendo anchura y altura, comparten el mismo eje x
fig, axes = plt.subplots(nrows=n, ncols=1, figsize=(14, max(2, 1.6 * n)), sharex=True)
# Dibuja la serie temporal de cada columna, pone su nombre al eje y y coloca un grid
for ax, col in zip(axes, energia.columns):
ax.plot(energia.index, energia[col], linewidth=1)
ax.set_ylabel(col)
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Coloca en la última gráfica el eje X
axes[-1].set_xlabel("Tiempo")
# Nombre general
fig.suptitle("Series temporales del barrio — cada serie en su propia fila", y=0.995)
# Distancia entre filas
plt.tight_layout(rect=[0, 0, 1, 0.99])
# Se muestra la gráfica
plt.show()
Intentamos analizar lo que vemos a simple vista. Hablaremos de muchos valores como estimación y no como número exacto, pero suficiente para entender como funciona cada variable:
Día de la semana: Esta variable simplemente indica el número del día de la semana en que nos encontramos, por eso la gráfica muestra esta forma periódica. Será nuestra variable exógena, por lo que no trataremos con ella y no la eliminaremos del dataset.
Electricidad (kW): Se puede ver que tiene una escala de 1e36. Al estar todos los valores en 0, la gráfica da a entender que hay una desproporción entre los datos y los outliers, ya que todos los valores están alrededor de 0 y unos pocos a 1e36, y otros a -2e36.
Fotovoltaica (kW): Casi todos los datos están entre 0 y 25000, mientras que algunos datos suben disparados a 100000, indicando presencia de outliers.
Refrigeración (kW): Sucede como en la variable Electricidad (kW): todos los datos se encuentra alrededor de 0, y algunos datos cerca de 1.5e36, por lo que estos outliers están desproporcionadamente por encima de los datos de la variable.
Calefacción (kWh): Casi todos los datos están alrededor de 0 y algunos a 2e33, pudiéndose ver de nuevo, outliers exageradamente desproporcionadamente grandes.
Energía total (kWh): Al ser la suma total de Electricidad (kW), Refrigeración (kWh) y Calefacción (kWh), y estas 3 tienen outliers, esta variable también tendrá outliers, y como puede verse en la gráfica, se ven iguales que Electricidad (kW), seguramente debido a que la escala de outliers es la más grande con 1e36 y -2e36.
Emisión (kg CO₂): Se puede ver que se parece bastante a "Energía total (kWh)", lo que tiene sentido según el contexto de la variable con el dataset, por lo que nos indica que puede haber una correlación muy alta con la variable objetivo.
Ahora vamos a analizar las variables de una en una, excepto los días de la semana, que como hemos visto, es exógena. Sustituiremos los valores atípicos que encontremos por valores nulos.
Electricidad (kW)¶
Al principio de todo, tratamos los valores nulos a partir de las otras 3 variables de las que depende. Para ello, las otras 3 columnas deberán estar presentes en aquella fila en la que esté el valor nulo localizado.
# Serie booleana que irá guardando a True que sean nulos la fila que queremos y no nulos las otras columnas en esa misma fila, con cada uno de los nulos
mask = (
energia['Electricidad (kW)'].isna() &
energia[['Refrigeración (kW)', 'Calefacción (kWh)', 'Energía total (kWh)']].notna().all(axis=1)
)
# Rellena los valores donde mask era igual a true
energia.loc[mask, 'Electricidad (kW)'] = (
energia.loc[mask, 'Energía total (kWh)'] -
energia.loc[mask, 'Refrigeración (kW)'] -
energia.loc[mask, 'Calefacción (kWh)']
)
Vamos a revisar cómo están distribuidos sus valores.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(energia['Electricidad (kW)'].describe())
count 7.887100e+04 mean -3.530612e+31 std 1.127846e+34 min -2.050000e+36 25% 2.168270e+04 50% 2.515137e+04 75% 2.915756e+04 max 1.290000e+36 Name: Electricidad (kW), dtype: float64
Podemos ver cómo hay datos con valores excesivamente altos y bajos que no tienen sentido en la variable, por lo tanto establecemos percentiles para solamente eliminar los datos excesivos. Establecemos como límites los percentiles 0.3 y 99.9 %.
# establecemos los percentiles
cuartil_minimo = energia['Electricidad (kW)'].quantile(0.003)
cuartil_maximo = energia['Electricidad (kW)'].quantile(0.999)
# Conservamos únicamente los datos en los cuales hemos establecido los límites
energia['Electricidad (kW)'] = energia['Electricidad (kW)'].where(
(energia['Electricidad (kW)'] >= cuartil_minimo) & (energia['Electricidad (kW)'] <= cuartil_maximo)
)
Ajustamos para intentar eliminar valores excesivos y otros valores que bajen o suban y que no tengan sentido. Comprobamos cuántos valores hemos eliminado.
# Visualizamos la cantidad de nulos que tiene el dataset
energia.isna().sum()
Día de la semana 0 Electricidad (kW) 333 Fotovoltaica (kW) 17 Refrigeración (kW) 17 Calefacción (kWh) 17 Energía total (kWh) 17 Emisión (kg CO₂) 17 dtype: int64
Si no contamos los anteriores 17 valores missing, hemos eliminado 316 valores, los cuáles constituye un ~0.4% del dataset, lo que debería ser una pequeña cantidad para eliminar.
Utilizamos el método que hemos decidido antes usar con los valores nulos: el interpolado temporal, haciendo que queden valores más lógicos.
Vamos a ver cómo hace la sustitución en una de las zonas con nulos, para ver cómo funciona la imputación por interpolación temporal. Para ello, graficamos uno de los huecos más grandes que hemos encontrado con valores missing.
# Se guarda una copia del dataset original
energia_original = energia.copy()
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
energia['Electricidad (kW)'] = energia['Electricidad (kW)'].interpolate(method='time')
# Guardamos la columna utilizada para el interpolado temporal
col = 'Electricidad (kW)'
# Guarda los índices donde hayan NaN de la columna seleccionada
idx_nan = energia_original[col][energia_original[col].isna()].index
# Elige el primer hueco
t0 = idx_nan[2]
# Zoom a la zona donde está el hueco, 12 horas antes y 12 horas después
inicio = t0 - pd.Timedelta(hours=12)
fin = t0 + pd.Timedelta(hours=12)
# Subconjunto de la serie temporal, con la original y la que aplicamos la interpolación, en las zonas calculadas anteriormente, en la columna seleccionada
orig_zoom = energia_original.loc[inicio:fin, col]
interp_zoom = energia.loc[inicio:fin, col]
# Se guarda una serie booleana con los nulos del subconjunto original
mask = orig_zoom.isna()
# Se crea una gráfica tamaño (10, 4)
plt.figure(figsize=(10,4))
# Se utilizará el índice de tiempo en el eje x y los valores originales de la variable en el eje y, marcándolos con círculos
plt.plot(orig_zoom.index, orig_zoom, marker='o')
plt.title('Serie temporal con hueco (valores nulos)')
plt.xlabel('Tiempo')
plt.ylabel(col)
plt.tight_layout()
plt.show()
Aquí podemos ver que los puntos indican los valores exactos de la variable, y las líneas de la función sirven como ayudas visuales. Como se puede ver, hay un hueco entre las 3 y las 9 de ese día. Ese hueco son los NaN que trataremos de rellenar con nuestra interpolación temporal. Para ver cómo se comporta, haremos otra gráfica.
# Copia el subconjunto que vamos a utilizar como interpolado
serie_azul = interp_zoom.copy()
# En el hueco interpolado, ponemos NaN para que no se dibuje azul
serie_azul[mask] = float('nan')
# Se crea una gráfica de tamaño (10, 4)
plt.figure(figsize=(10,4))
# Se pintará en azul los datos originales, sin pintar el corte, con el indice de tiempo como eje x y los valores en el eje y
plt.plot(
serie_azul.index,
serie_azul,
color='steelblue',
label='Datos originales'
)
# Se pintará en naranja en todas las zonas que han sido seleccionadas como nulas
idx_interp = interp_zoom.index[mask]
# Se añade un punto antes y después para continuidad visual
idx_ext = interp_zoom.index[
interp_zoom.index.get_loc(idx_interp[0]) - 1 :
interp_zoom.index.get_loc(idx_interp[-1]) + 2
]
# Se pinta en la gráfica de color naranja la zona cortada
plt.plot(
idx_ext,
interp_zoom.loc[idx_ext],
color='orange',
linewidth=3,
label='Tramo interpolado'
)
# Se imprime la serie temporal
plt.title('Serie temporal con hueco rellenado por interpolación')
plt.xlabel('Tiempo')
plt.ylabel(col)
plt.legend()
plt.tight_layout()
plt.show()
En esta segunda gráfica, vemos cómo el código anterior ha ocupado los huecos de los datos faltantes. Vemos cómo el interpolado temporal se comporta como una interpolación lineal tal y como ya dijimos. Vemos que se produce un relleno de datos bastante aceptable al usar este método, por lo que lo usaremos también para el resto de series.
Aquí podemos ver cómo han quedado los datos después del interpolado temporal.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
energia['Electricidad (kW)'].describe()
count 78888.000000 mean 25705.182745 std 5426.466777 min 14614.250000 25% 21681.755000 50% 25145.835000 75% 29146.885000 max 43837.690000 Name: Electricidad (kW), dtype: float64
Se puede ver un mínimo de 14614.25, un máximo de 43837.69, una mediana de 25145.835 y una desviación típica de 5426.47.
Ahora vamos a analizar la gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(energia.index, energia['Electricidad (kW)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Electricidad (kW)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Electricidad')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Vemos cómo los valores parecen ir desde alrededor de 14000 a alrededor de 43000, valores que parecen más típicos para el contexto del problema. Además, ahora podemos ver correctamente la representación de la gráfica, sin valores "aplastados".
Por último, para confirmar que no quedan datos atípicos que resulten exagerados, ahora vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=energia.index.month, y='Electricidad (kW)', data=energia)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Electricidad (kW)')
plt.title('Boxplot mensual de Electricidad (kW)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver como aumenta el consumo de electricidad en meses de verano, como sucedía con energía total.
Fotovoltaica (kW)¶
Vamos a analizar la variable Fotovoltaica (kW)
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(energia['Fotovoltaica (kW)'].describe())
count 78871.000000 mean 3968.465261 std 5310.401284 min 0.000000 25% 3.870000 50% 159.940000 75% 8394.125000 max 111938.620000 Name: Fotovoltaica (kW), dtype: float64
Se puede ver, cómo el mínimo es de 0, el valor del primer cuartil de 3.87, la mediana de 159.94, el valor del tercer cuartil de 8394.125 y el máximo de 111938.62. Por lo que se ve, y por lo que hemos visto en la gráfica, hay outliers muy altos que debemos eliminar. Para ello utilizaremos el mismo proceso que la anterior variable pero sin tener en cuenta el percentil inferior.
# establecemos los percentiles
cuartil_maximo = energia['Fotovoltaica (kW)'].quantile(0.9999)
# Conservamos únicamente los datos en los cuales hemos establecido los límites
energia['Fotovoltaica (kW)'] = energia['Fotovoltaica (kW)'].where((energia['Fotovoltaica (kW)'] <= cuartil_maximo)
)
Una vez eliminado únicamente los outliers, revisamos cuántos outliers hemos eliminado.
# Visualizamos la cantidad de nulos que tiene el dataset
energia.isna().sum()
Día de la semana 0 Electricidad (kW) 0 Fotovoltaica (kW) 25 Refrigeración (kW) 17 Calefacción (kWh) 17 Energía total (kWh) 17 Emisión (kg CO₂) 17 dtype: int64
Si no contamos los 17 nulos ya presentes, hemos eliminado 8 outliers. Ahora vamos a hacer un interpolado temporal para rellenar nulos siguiendo la distribución de la gráfica.
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
energia['Fotovoltaica (kW)'] = energia['Fotovoltaica (kW)'].interpolate(method='time')
Revisamos cómo han quedado los datos despés del interpolado temporal.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(energia['Fotovoltaica (kW)'].describe())
count 78888.000000 mean 3965.668666 std 5291.384048 min 0.000000 25% 3.870000 50% 160.065000 75% 8392.707500 max 19031.330000 Name: Fotovoltaica (kW), dtype: float64
Vemos como ya no quedan nulos (count 78888 datos), y la variable parece tener un mínimo de 0, un máximo de 19031.33, una mediana de 160.065 y una desviación típica de 5291.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(energia.index, energia['Fotovoltaica (kW)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Fotovoltaica (kW)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Fotovoltaica')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Se puede ver cómo podemos ver correctamente la gráfica que habíamos visto al principio pero sin los outliers, por lo tanto se ve que todos los outliers han sido eliminados de forma definitiva.
Ahora vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=energia.index.month, y='Fotovoltaica (kW)', data=energia)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Fotovoltaica (kW)')
plt.title('Boxplot mensual de Fotovoltaica (kW)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver como la generación de energía fotovoltaica es mayor en los meses de marzo, abril y mayo.
Refrigeración (kW)¶
Al principio de todo, tratamos los valores nulos a partir de las otras 3 variables de las que depende. Para ello, las otras 3 columnas deberán estar presentes en aquella fila en la que esté el valor nulo localizado.
# Serie booleana que irá guardando a True que sean nulos la fila que queremos y no nulos las otras columnas en esa misma fila, con cada uno de los nulos
mask = (
energia['Refrigeración (kW)'].isna() &
energia[['Electricidad (kW)', 'Calefacción (kWh)', 'Energía total (kWh)']].notna().all(axis=1)
)
# Rellena los valores donde mask era igual a true
energia.loc[mask, 'Refrigeración (kW)'] = (
energia.loc[mask, 'Energía total (kWh)'] -
energia.loc[mask, 'Electricidad (kW)'] -
energia.loc[mask, 'Calefacción (kWh)']
)
Analizamos la variable Refrigeración (kW).
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(energia['Refrigeración (kW)'].describe())
count 7.887100e+04 mean 2.952257e+04 std 1.725418e+04 min 0.000000e+00 25% 1.549117e+04 50% 2.629496e+04 75% 4.134383e+04 max 1.468215e+06 Name: Refrigeración (kW), dtype: float64
Aquí se puede ver, una vez más, que el valor máximo es un número mucho más grande que el resto de valores, indicando que hay outliers hacia arriba, por lo tanto aplicaremos la misma metodología de la variable anterior.
Para ello eliminaremos (sustituidos por nulos) todos los datos que esten por encima del percentil que establecemos (99.99%), para deshacernos de los outliers. No usamos límite inferior.
# establecemos los percentiles
cuartil_maximo = energia['Refrigeración (kW)'].quantile(0.9999)
# Conservamos únicamente los datos en los cuales hemos establecido los límites
energia['Refrigeración (kW)'] = energia['Refrigeración (kW)'].where((energia['Refrigeración (kW)'] <= cuartil_maximo)
)
Comprobamos cuántos nulos tiene nuestro dataset al eliminar los outliers.
# Visualizamos la cantidad de nulos que tiene el dataset
energia.isna().sum()
Día de la semana 0 Electricidad (kW) 0 Fotovoltaica (kW) 0 Refrigeración (kW) 25 Calefacción (kWh) 17 Energía total (kWh) 17 Emisión (kg CO₂) 17 dtype: int64
Si no contamos los 17 valores missing anteriores, se ve cómo hemos eliminado 8 outliers. Ahora realizaremos la imputacion por interpolación temporal, como hicimos en la variable anterior.
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
energia['Refrigeración (kW)'] = energia['Refrigeración (kW)'].interpolate(method='time')
Ahora comprobamos cómo ha quedado nuestra variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(energia['Refrigeración (kW)'].describe())
count 78888.000000 mean 29491.587231 std 16349.567109 min 0.000000 25% 15484.964130 50% 26290.507005 75% 41338.870755 max 80437.553190 Name: Refrigeración (kW), dtype: float64
Se puede ver cómo todos los nulos han sido sustituidos por los valores de interpolación temporal (tenemos 7888 valores no nulos) y vemos cómo su mínimo es de 0, su máximo es de 80437.55319, su mediana es de 26290.507005, y su desviación típica es de 16349.57.
Ahora revisaremos cómo se ve en la gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(energia.index, energia['Refrigeración (kW)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Refrigeración (kW)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Refrigeración')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Se puede ver cómo hay algunas bajadas sospechosas de valores en la gráfica, pero no las vemos lo suficientemente graves para tener que tratarlas.
Ahora vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=energia.index.month, y='Refrigeración (kW)', data=energia)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Refrigeración (kW)')
plt.title('Boxplot mensual de Refrigeración (kW)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver un aumento de Refrigeración consumida en los meses de verano.
Calefacción (kWh)¶
Al principio de todo, tratamos los valores nulos a partir de las otras 3 variables de las que depende. Para ello, las otras 3 columnas deberán estar presentes en aquella fila en la que esté el valor nulo localizado.
# Serie booleana que irá guardando a True que sean nulos la fila que queremos y no nulos las otras columnas en esa misma fila, con cada uno de los nulos
mask = (
energia['Calefacción (kWh)'].isna() &
energia[['Electricidad (kW)', 'Refrigeración (kW)', 'Energía total (kWh)']].notna().all(axis=1)
)
# Rellena los valores donde mask era igual a true
energia.loc[mask, 'Calefacción (kWh)'] = (
energia.loc[mask, 'Energía total (kWh)'] -
energia.loc[mask, 'Electricidad (kW)'] -
energia.loc[mask, 'Refrigeración (kW)']
)
Analizamos la variable Calefacción (kWh).
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(energia['Calefacción (kWh)'].describe())
count 7.887100e+04 mean 3.136308e+28 std 8.807580e+30 min -2.930710e+00 25% 1.462424e+03 50% 1.828763e+03 75% 2.617124e+03 max 2.473519e+33 Name: Calefacción (kWh), dtype: float64
Podemos ver cómo el valor máximo es demasiado alto e irreal, y el número mínimo es negativo cuando no tiene sentido en el contexto del problema, por lo que procedemos a eliminarlos (sustituyéndolos por nulos), con el método de los percentiles, tanto superior como inferior. Ahora usamos como percentil inferior, 0.001, y como percentil superior, 99.9.
# establecemos los percentiles
cuartil_minimo = energia['Calefacción (kWh)'].quantile(0.0001)
cuartil_maximo = energia['Calefacción (kWh)'].quantile(0.999)
# Conservamos únicamente los datos en los cuales hemos establecido los límites
energia['Calefacción (kWh)'] = energia['Calefacción (kWh)'].where(
(energia['Calefacción (kWh)'] >= cuartil_minimo) & (energia['Calefacción (kWh)'] <= cuartil_maximo)
)
Ahora revisaremos la cantidad de outliers que hemos eliminado.
# Visualizamos la cantidad de nulos que tiene el dataset
energia.isna().sum()
Día de la semana 0 Electricidad (kW) 0 Fotovoltaica (kW) 0 Refrigeración (kW) 0 Calefacción (kWh) 102 Energía total (kWh) 17 Emisión (kg CO₂) 17 dtype: int64
Si no contamos los anteriores 17 nulos, vemos que hemos eliminado 85 outliers. Para rellenar nulos utilizaremos la interpolación temporal.
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
energia['Calefacción (kWh)'] = energia['Calefacción (kWh)'].interpolate(method='time')
Ahora veremos como ha quedado el dataset después del interpolado temporal.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
energia['Calefacción (kWh)'].describe()
count 78888.000000 mean 2162.803128 std 1038.985587 min 0.000000 25% 1462.424290 50% 1828.763040 75% 2614.193320 max 8370.107760 Name: Calefacción (kWh), dtype: float64
Podemos ver cómo ya no quedan nulos (78888 datos), y podemos ver como el mínimo es de 0, el máximo es de 8370.10776, su mediana es de 1828.763040, y su desviación típica es de 1039.
Ahora revisaremos la gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(energia.index, energia['Calefacción (kWh)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Calefacción (kWh)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Calefacción')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Se puede ver cómo la gráfica muestra un comportamiento más correcto por lo general, aunque vemos un comportamiento raro al poco tiempo de comenzar el año 2022, posiblemente debido a factores internos o externos al sistema, como incidencias técnicas, ajustes operativos o periodos de menor ocupación. Es posible que hubiera habido fallos en los sistemas de calefacción durante ese período de tiempo.
Ahora revisaremos los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=energia.index.month, y='Calefacción (kWh)', data=energia)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Calefacción (kWh)')
plt.title('Boxplot mensual de Calefacción (kWh)')
plt.show()
Podemos ver que sí que tenemos outliers más pronunciados, pero si nos fijamos en las gráficas, estos probablemente se deban a que en los primeros años el consumo de calefacción es mucho más excesivo. Por esto, creemos que pueden no deberse a medidas erróneas y decidimos no tratarlos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, sólo quitaremos los *hard outliers***
Además, podemos ver una disminución de la calefacción consumida en los meses de verano.
Energía total (kWh)¶
Vamos a analizar la variable Energía total.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(energia['Energía total (kWh)'].describe())
count 7.887100e+04 mean -3.529332e+31 std 1.128069e+34 min -2.051497e+36 25% 4.058740e+04 50% 5.310153e+04 75% 7.177602e+04 max 1.289512e+36 Name: Energía total (kWh), dtype: float64
Como podemos ver en el enunciado de la práctica, la variable "Energía total (kWh)" es la suma total de las variables "Electricidad (kW)", "Refrigeración (kW)" y "Calefacción (kWh)", por lo que trataremos los outliers reemplazando los valores que no nos coincidan con la suma de las 3 variables.
# Calcula la suma correcta por fila
suma_energia = (
energia['Electricidad (kW)'] +
energia['Refrigeración (kW)'] +
energia['Calefacción (kWh)']
)
# Reemplaza solo donde no coincide
energia.loc[
energia['Energía total (kWh)'] != suma_energia,
'Energía total (kWh)'
] = suma_energia
Una vez calculada, comprobaremos cómo ha quedado nuestra variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
energia['Energía total (kWh)'].describe()
count 78888.000000 mean 57359.573104 std 20444.996954 min 17624.088000 25% 40587.175845 50% 53075.334005 75% 71717.465835 max 125635.475740 Name: Energía total (kWh), dtype: float64
Vemos cómo nuestro dataset ya contiene valores aceptables y ningún nulo. Además se puede ver que tiene un mínimo de 17624.088, un máximo es de 125635.47574, una mediana de 53075.334005 y una desviación típica de 20445.
Ahora vamos a revisar la gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(energia.index, energia['Energía total (kWh)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Energía total (kWh)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Energía total')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Vemos cómo la gráfica parece funcionar de manera correcta.
Por último, vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=energia.index.month, y='Energía total (kWh)', data=energia)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Energía total (kWh)')
plt.title('Boxplot mensual de Energía total (kWh)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. También se ve un aumento en meses de verano el consumo de energía total.
Emisión (kg CO₂)¶
Vamos a analizar cómo funciona la variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(energia['Emisión (kg CO₂)'].describe())
count 7.887100e+04 mean -2.198426e+31 std 7.032879e+33 min -1.280000e+36 25% 2.531000e+04 50% 3.312000e+04 75% 4.476000e+04 max 8.050000e+35 Name: Emisión (kg CO₂), dtype: float64
Vemos que tenemos unos datos extremadamente grandes y extremadamente negativos para el máximo y el mínimo de la variable. En la variable no tienen sentido este tipo de datos, por lo que estableceremos percentiles superior e inferior para tratarlos. Ahora usamos 0.02 como percentil inferior y 99.9 como percentil superior.
# establecemos los percentiles
cuartil_minimo = energia['Emisión (kg CO₂)'].quantile(0.002)
cuartil_maximo = energia['Emisión (kg CO₂)'].quantile(0.999)
# Conservamos únicamente los datos en los cuales hemos establecido los límites
energia['Emisión (kg CO₂)'] = energia['Emisión (kg CO₂)'].where(
(energia['Emisión (kg CO₂)'] >= cuartil_minimo) & (energia['Emisión (kg CO₂)'] <= cuartil_maximo)
)
Comprobamos la cantidad de outliers detectados y eliminados.
# Visualizamos la cantidad de nulos que tiene el dataset
energia.isna().sum()
Día de la semana 0 Electricidad (kW) 0 Fotovoltaica (kW) 0 Refrigeración (kW) 0 Calefacción (kWh) 0 Energía total (kWh) 0 Emisión (kg CO₂) 254 dtype: int64
Sin contar los 17 nulos anteriores, vemos que tenemos 237 elementos que hemos eliminado, por lo que ahora imputaremos por interpolación temporal.
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
energia['Emisión (kg CO₂)'] = energia['Emisión (kg CO₂)'].interpolate(method='time')
Vemos cómo ha quedado el dataset y si ha quedado algún nulo sin rellenar.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
energia['Emisión (kg CO₂)'].describe()
count 78888.000000 mean 35787.470464 std 12744.032516 min 14260.000000 25% 25330.000000 50% 33120.000000 75% 44750.000000 max 74540.000000 Name: Emisión (kg CO₂), dtype: float64
Vemos cómo no hay nulos (78888 datos) y sus datos tienen valores lógicos con el resto de valores de la variable. Tenemos un mínimo de 14260, un máximo de 74540, una mediana de 33120 y una desviación típica de 12744.
Ahora veremos su representación temporal en una gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(energia.index, energia['Emisión (kg CO₂)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Emisión (kg CO₂)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Emisión (kg CO₂)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Vemos cómo la gráfica por lo general funciona bastante bien. Más adelante haremos un análisis más profundo de cada una de estas series y procederemos a la caracterización de cada una de ellas.
Ahora revisaremos los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=energia.index.month, y='Emisión (kg CO₂)', data=energia)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Emisión (kg CO₂)')
plt.title('Boxplot mensual de Emisión (kg CO₂)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver un aumento de emisión de gases de efecto invernadero en los meses de verano y septiembre.
Tabla de correlación¶
Ahora vamos a estudiar las correlaciones del dataset.
En primer lugar vamos a revisar la varianza.
# Calcular varianza de todas las columnas numéricas
varianza = energia.var()
varianza
Día de la semana 3.997008e+00 Electricidad (kW) 2.944654e+07 Fotovoltaica (kW) 2.799875e+07 Refrigeración (kW) 2.673083e+08 Calefacción (kWh) 1.079491e+06 Energía total (kWh) 4.179979e+08 Emisión (kg CO₂) 1.624104e+08 dtype: float64
No hay ninguna varianza cercana a 0, por lo tanto no eliminamos nada.
Ahora revisamos qué variables tienen más correlación con la variable objetivo "Energía total (kWh)".
# Hacemos una matriz de correlación y visualizaremos sobre nuestra columna objetivo cuáles columnas parecen ser la más influyentes
matriz_correlacion = energia.corr()
# Visualizaremos de mayor a menor
matriz_correlacion["Energía total (kWh)"].sort_values(ascending=False)
Energía total (kWh) 1.000000 Emisión (kg CO₂) 0.998728 Refrigeración (kW) 0.985781 Electricidad (kW) 0.899219 Fotovoltaica (kW) 0.347314 Día de la semana -0.079916 Calefacción (kWh) -0.530983 Name: Energía total (kWh), dtype: float64
Vemos que en "Emisión (kg CO₂)", tal y como habíamos visto cuando explicábamos las gráficas de las variables sin aún haber eliminado outliers, hay una correlación casi del 100%. De momento, no la tocaremos hasta ver más detalles en la matriz de correlación.
Otras variables con mucha correlación son: Refrigeración (kW), Electricidad (kW) y Calefacción (kWh). Son las 3 variables que utilizamos para calcular la variable objetivo, por lo tanto tiene sentido que haya una alta correlación y no las tocaremos.
Por último, vemos que "Día de la semana" tiene muy poca correlación con nuestra variable objetivo, pero no la eliminaremos ya que nos puede seguir siendo útil en series temporales, como es el caso de capturar patrones semanales.
Por último, vamos a revisar la matriz de correlación.
# Crear la tabla de correlación
correlation_matrix = energia.corr()
plt.figure(figsize=(10,8))
sns.heatmap(correlation_matrix, annot=True, fmt=".2f", cmap="coolwarm")
plt.title("Matriz de correlación")
plt.show()
Como podemos ver, se dan bastantes casos de alta correlación entre variables, destacando sobre todo "Emisión (kg CO₂)" con una correlación de 1.00. Decidimos no eliminar nada, pese al peligro de que haya problemas de multicolinealidad, porque el dataset ya contiene muy pocas variables (7) y pensamos que puede seguir siéndonos útil.
Transformacion a días¶
Ante la gran cantidad de datos y el hecho de que el problema nos pide un horizonte temporal de 7 días, no tiene mucho sentido que los datos estén en formato horario. Por ello, decidimos cambiar la granularidad temporal de los datos para que sean diarios. Para ello, hacemos un resample diario que sume todos los datos de cada día, puesto que estamos agregandos datos totales de consumo y producción de energía para cada día dado. Debemos tener en cuenta que "Día de la semana" no queremos que cambie, y como cada fila es la suma de si mismo 24 veces, decidimos hacer una división de 24 y transformarlo en entero.
# Guardamos una copia
energia_horas = energia
# Cambia frecuencia temporal
energia = energia.resample('D').sum()
# Divide entre 24 los datos de la variable
energia['Día de la semana'] = energia['Día de la semana']/24
# Transforma a entero la variable
energia['Día de la semana'] = energia['Día de la semana'].astype(int)
Revisamos cómo han quedado las 10 primeras filas.
# Visualizamos las 5 primeras columnas del dataset
energia.head(10)
| Día de la semana | Electricidad (kW) | Fotovoltaica (kW) | Refrigeración (kW) | Calefacción (kWh) | Energía total (kWh) | Emisión (kg CO₂) | |
|---|---|---|---|---|---|---|---|
| 2014-01-01 | 2 | 490049.28 | 62795.47 | 243369.71770 | 20107.60131 | 753526.59901 | 469960.0 |
| 2014-01-02 | 3 | 556486.56 | 64284.00 | 323886.05169 | 23987.86135 | 904360.47304 | 564020.0 |
| 2014-01-03 | 4 | 545831.45 | 67187.06 | 337004.60237 | 22112.20695 | 904948.25932 | 564390.0 |
| 2014-01-04 | 5 | 511495.56 | 46798.28 | 252493.06189 | 21074.73561 | 785063.35750 | 489640.0 |
| 2014-01-05 | 6 | 502806.80 | 70162.87 | 249265.37031 | 22777.47812 | 774849.64843 | 483270.0 |
| 2014-01-06 | 0 | 543968.35 | 71666.60 | 285299.00483 | 27314.21720 | 856581.57203 | 534210.0 |
| 2014-01-07 | 1 | 562002.91 | 52517.04 | 274781.34599 | 23633.24544 | 860417.50143 | 536630.0 |
| 2014-01-08 | 2 | 557214.72 | 66145.06 | 265609.15067 | 24020.09916 | 846843.96983 | 528160.0 |
| 2014-01-09 | 3 | 563161.10 | 50888.52 | 243592.34380 | 23108.64835 | 829862.09215 | 517550.0 |
| 2014-01-10 | 4 | 520356.53 | 62773.62 | 235918.63667 | 22238.22748 | 778513.39415 | 485540.0 |
Revisamos cómo ha quedado nuestro dataset en general.
# Información del dataset
energia.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 3287 entries, 2014-01-01 to 2022-12-31 Freq: D Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Día de la semana 3287 non-null int64 1 Electricidad (kW) 3287 non-null float64 2 Fotovoltaica (kW) 3287 non-null float64 3 Refrigeración (kW) 3287 non-null float64 4 Calefacción (kWh) 3287 non-null float64 5 Energía total (kWh) 3287 non-null float64 6 Emisión (kg CO₂) 3287 non-null float64 dtypes: float64(6), int64(1) memory usage: 205.4 KB
Vemos cómo nuestro dataset ha pasado de 78888 a 3287 entradas (78888/24) y todo lo demás queda igual. Ahora estos datos son mucho más manejables y coinciden con la granularidad temporal que nos exige el problema.
Análisis de las variables¶
Hay que tener en cuenta que aunque en varias partes de los análisis de las variables tengamos que ir cambiando su granularidad (para que se vean más suavizadas las lineas temporales de las variables y podamos interpretarlas com mayor sencillez), siempre tendremos frecuencia diaria en nuestro dataset, y esto no cambiará.
Vamos a analizar sobre todas nuestras variables, su tendencia, estacionalidad, ruido, estacionariedad y autocorrelación. Así conseguiremos caracterizar cada una de las series del dataset.
Energía total (kWh)¶
Primero empezamos con la variable objetivo, ya que es la más importante de nuestro dataset.
# Creamos una gráfica de tamaño (12,4) de la variable Energía total (kWh)
plt.figure(figsize=(12,4))
energia['Energía total (kWh)'].plot()
plt.show()
Se puede ver a simple vista que la serie es bastante densa, posiblemente porque aún tenemos bastantes datos, por lo tanto vamos a realizar una reducción de granularidad a semana, mes y año para analizar esta variable con más facilidad.
# Reduce granularidad a semanal de la variable
energia_semanal = energia['Energía total (kWh)'].resample('W').sum()
# Reduce granularidad a mensual de la variable
energia_mensual = energia['Energía total (kWh)'].resample('ME').sum()
# Reduce granularidad a anual de la variable
energia_anual = energia['Energía total (kWh)'].resample('YE').sum()
# Crea una gráfica (12,4) con granularidad semanal de la variable
plt.figure(figsize=(12,4))
energia_semanal.plot()
plt.show()
# Crea una gráfica (12,4) con granularidad semanal de la variable
plt.figure(figsize=(12,4))
energia_mensual.plot()
plt.show()
# Crea una gráfica (12,4) con granularidad semanal de la variable
plt.figure(figsize=(12,4))
energia_anual.plot()
plt.show()
Con granularidad anual podemos ver claramente la tendencia en el tiempo de la variable, en la cual parece haber una tendencia ascendente desde 2014 hasta 2018, luego decrementa fuertemente desde 2018 hasta 2020, y luego sigue decrementando desde 2020 hasta 2022 de forma mucho menos notoria.
Ahora vamos a revisar si hay alguna posible estacionalidad, y para que se vea de forma más o menos clara, dibujaremos las gráficas cada 3 años.
# Selección de rango
ts = energia_mensual["2014":"2016"]
# Crear la figura
fig, ax = plt.subplots(figsize=(12,4))
# Graficar línea principal
ax.plot(ts.index, ts.values, marker='o', color='blue', label='Consumo mensual')
# Resaltar el pico de cada año
for year in ts.index.year.unique():
pico = ts[ts.index.year == year].idxmax()
ax.scatter(pico, ts[pico], color='red', zorder=5)
ax.text(pico, ts[pico]+0.1, str(pico.month), ha='center', color='red')
# Título y etiquetas
ax.set_title("Consumo medio mensual (2014-2016) con picos estacionales")
ax.set_xlabel("Mes")
ax.set_ylabel("Energía total (kWh)")
# Grid
ax.grid(color='gray', linestyle='--', linewidth=0.7)
# Configurar eje X para mostrar todos los meses
ax.xaxis.set_major_locator(mdates.MonthLocator()) # cada mes
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
# Rotar etiquetas para que no se superpongan
plt.xticks(rotation=45)
# Leyenda
ax.legend()
plt.tight_layout()
plt.show()
# Selección de rango
ts = energia_mensual["2017":"2019"]
# Crear la figura
fig, ax = plt.subplots(figsize=(12,4))
# Graficar línea principal
ax.plot(ts.index, ts.values, marker='o', color='blue', label='Consumo mensual')
# Resaltar el pico de cada año
for year in ts.index.year.unique():
pico = ts[ts.index.year == year].idxmax()
ax.scatter(pico, ts[pico], color='red', zorder=5)
ax.text(pico, ts[pico]+0.1, str(pico.month), ha='center', color='red')
# Título y etiquetas
ax.set_title("Consumo medio mensual (2017-2019) con picos estacionales")
ax.set_xlabel("Mes")
ax.set_ylabel("Energía total (kWh)")
# Grid
ax.grid(color='gray', linestyle='--', linewidth=0.7)
# Configurar eje X para mostrar todos los meses
ax.xaxis.set_major_locator(mdates.MonthLocator()) # cada mes
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
# Rotar etiquetas para que no se superpongan
plt.xticks(rotation=45)
# Leyenda
ax.legend()
plt.tight_layout()
plt.show()
# Selección de rango
ts = energia_mensual["2020":"2022"]
# Crear la figura
fig, ax = plt.subplots(figsize=(12,4))
# Graficar línea principal
ax.plot(ts.index, ts.values, marker='o', color='blue', label='Consumo mensual')
# Resaltar el pico de cada año
for year in ts.index.year.unique():
pico = ts[ts.index.year == year].idxmax()
ax.scatter(pico, ts[pico], color='red', zorder=5)
ax.text(pico, ts[pico]+0.1, str(pico.month), ha='center', color='red')
# Título y etiquetas
ax.set_title("Consumo medio mensual (2020-2022) con picos estacionales")
ax.set_xlabel("Mes")
ax.set_ylabel("Energía total (kWh)")
# Grid
ax.grid(color='gray', linestyle='--', linewidth=0.7)
# Configurar eje X para mostrar todos los meses
ax.xaxis.set_major_locator(mdates.MonthLocator()) # cada mes
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
# Rotar etiquetas para que no se superpongan
plt.xticks(rotation=45)
# Leyenda
ax.legend()
plt.tight_layout()
plt.show()
Se puede ver que existe una posible estacionalidad anual, puesto que que parece que sube a partir de junio y baja a partir de septiembre u octubre, bajando de manera consistente hasta enero, cada año. Luego revisaremos la descomposición de la serie (método decompose) para confirmarlo.
Ahora vamos a analizar el histograma.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia['Energía total (kWh)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 700000 y 1400000, haciendo que la mayor parte de Energía total (kWh) esté entre la parte baja y mediana del histograma, haciendo que haya una cola hacia la derecha y por lo tanto, un sesgo positivo, que implica que no es una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a analizar los componentes que tenemos al descomponer la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia['Energía total (kWh)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que parece haber una tendencia al principio ascedente y luego, claramente descendente. Parece haber estacionalidad anual y el ruido parece tener variabilidad constante y estar centrado en 0.
Después de varias pruebas, vemos cómo la tendencia y estacionalidad se pueden ver muy bien con un periodo anual (30*12). Primero revisaremos la tendencia, como habíamos hecho antes, donde había un ascenso, luego un descenso, y luego otro descenso mucho más lento.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad anual, se puede ver una tendencia que incrementa desde 2014 hasta 2018, y luego que desciende desde 2018 hasta 2022.
Comprobaremos la estacionalidad.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Pese a que tenemos nuestro Seasonal en periodo anual, lo describimos a través de granularidad mensual. Podemos ver que, efectivamente, hay una estacionalidad en esta variable, ya que hay repeticiones del mismo patrón, haciendo que esta variable pueda ser fácilmente predecible ya que sabemos que se irá repitiendo cada año.
Ahora vamos a analizar el ruido.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Analizando el residual podemos ver cómo no recoge estacionalidad, oscila alrededor de 0 por lo que decompose está hecho de forma correcta. No hay tendencia, hay una variabilidad mas o menos constante, y no se nota la presencia de outliers.
Ahora vamos a revisar a ver si hay estacionalidad semanal en la variable objetivo.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia['Energía total (kWh)'], model='additive', period=7)
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Podemos ver cómo también hay una estacionalidad semanal, ya que hay una repetición de patrones a lo largo del tiempo entre semanas.
Si lo miramos con más exactitud:
# Descomponer toda la serie
resultado = seasonal_decompose(energia['Energía total (kWh)'], model='additive', period=7)
# Definir número de días a mostrar
dias = 70
# Crear gráfico manual con solo los primeros X días
fig, axes = plt.subplots(1, 1, figsize=(12, 4))
resultado.seasonal[:dias].plot( title='Seasonal')
plt.tight_layout()
plt.show()
Si miramos con granularidad diaria los primeros 70 días de la serie, podemos ver con mayor exactitud la estacionalidad. Nuestra serie comienza en miércoles. Teniendo eso en cuenta, el primer pico inferior sería un domingo. A partir de este pico inferior, la serie muestra cómo los días laborales de la semana el consumo es mayor y en fin de semana desciende.
Ahora vamos a hablar de la estacionariedad. Para ello aplicaremos el test estadístico de Dickey-Fuller.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia['Energía total (kWh)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -3.969350 p-value: 0.001580 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El p-valor es 0.001590 < 0.05, por lo que se rechaza la hipótesis nula, por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto, al ser estacionaria, decidimos no realizar ninguna transformación sobre nuestra serie temporal ni aplicar diferenciación cuando construyamos nuestos modelos predictivos.
Ahora revisaremos la autocorrelación con lags=7, puesto que se nos pide un horizonte temporal de 7 días de la energía total consumida.
plt.figure(figsize=(10, 6))
sm.graphics.tsa.plot_acf(energia['Energía total (kWh)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Se puede ver que hay una autocorrelación muy alta en todos los puntos del lag, todos encima del 75%, indicando una dependencia entre los valores presentes y pasados. Es decir, existe una gran correlación de cada punto de la series con los datos de la semana anterior.
Vemos cómo es la autocorrelación en un período anual.
# Se crea una gráfcia de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Energía total (kWh)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que en un mes (30 días), la autocorrelación sigue siendo alta. A partir de la mitad del año (180 días aprox.), existe una autorrelación negativa destacable. Esto puede confirmarnos, tal y como se puede ver en las gráficas, que el valor de la serie durante la mitad del año baja con la misma fuerza con la que subió con durante la otra mitad. Con lag=365 (un año) confirmamos también que hay mucha autocorrelación y por tanto, que hay estacionalidad.
Por tanto, en vista de estos datos, veremos cómo se comportan nuestros modelos predictivos cuando usemos como valores de lag 7 días, 30, 183 y 365.
Electricidad (kW)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia['Electricidad (kW)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 450000 y 750000, haciendo que la mayor parte de Electricidad (kW) esté entre la parte baja y mediana superior del histograma, haciendo que haya una cola hacia la derecha y por lo tanto, un sesgo positivo, que implica que no es una distribución normal. Vemos cómo aquí también la distribución de los datos parece ser bastante ancha.
Ahora vamos a revisar el resto de componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia['Electricidad (kW)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que parece haber una tendencia al principio ascedente y luego, claramente descendente. Parece haber estacionalidad anual y el ruido parece tener variabilidad constante y estar centrado en 0.
Establecemos un periodo anual porque es donde podemos ver bien la tendencia, estacionalidad y ruido de forma aceptable.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad anual, vemos en la tendencia un ascenso desde 2014 a 2016. Mantiene su valor hasta 2018 y a partir de 2018 desciende.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver cómo hay estacionalidad de forma clara, por lo que seguramente se repetirá el patrón en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, en cuanto al ruido, ronda alrededor de 0, no parece haber estacionalidad, ni outliers ni tendencia, y al principio de la gráfica, en los primeros años, hay más ruido que al final.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia['Electricidad (kW)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -3.425536 p-value: 0.010119 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
Tras aplicar el test de Dickey-Fuller, obtenemos un p-valor de 0.010119 < 0.05, por lo que se rechaza la hipótesis nula. Por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Electricidad (kW)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
La autocorrelación se mantiene por encima del 75% haciendo que haya dependencia entre valores pasados y presentes para 7 días. Es decir, cada dato tiene una alta correlación con los 7 datos anteriores.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Electricidad (kW)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que hasta poco más de un mes (30 días), la autocorrelación sigue siendo alta. Con lag=365 (un año) confirmamos también que hay mucha autocorrelación y por tanto, que hay estacionalidad.
Fotovoltaica (kW)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia['Fotovoltaica (kW)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 60000 y 140000, haciendo que la mayor parte de Fotovoltaica (kW) esté entre la parte mediana inferior y superior del histograma, haciendo que haya una cola hacia la izquierda y por lo tanto, un sesgo negativo, que implica que no llega a ser del todo una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a revisar los componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia['Fotovoltaica (kW)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que parece haber una tendencia no muy clara, más bien descendente. Parece haber estacionalidad anual y el ruido parace tener variabilidad constante y estar centrado en 0. Parece haber bastantes datos con ruido.
Elegimos el periodo anual porque podemos ver bien la tendencia, la estacionalidad y el ruido.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad anual, podemos ver un ascenso desde 2014 hasta 2016, un descenso hasta 2018, un ascenso hasta 2020, y luego un fuerte descenso.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver cómo existe una estacionalidad anual, por lo que el patrón se repetirá periódicamente.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, podemos ver cómo no hay una tendencia, no hay estacionalidad, la gráfica ronda el 0, no hay outliers y hay menos ruido al principio que al final.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia['Fotovoltaica (kW)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -3.279122 p-value: 0.015838 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El estadístico de Dickey-Fuller nos da un p-valor de 0.015838 < 0.05, por lo que se rechaza la hipótesis nula. Por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Fotovoltaica (kW)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
En cuanto a la autocorrelación, podemos ver como hay una correlación mayor del 60% por lo que hay una fuerte dependencia entre valores pasados y presentes para 7 días.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Fotovoltaica (kW)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que hasta poco más de un mes (30 días), la autocorrelación sigue siendo alta. A partir de la mitad del año (180 días aprox.), existe una autorrelación negativa apreciable. Con lag=365 (un año) confirmamos también que hay mucha autocorrelación y por tanto, que hay estacionalidad.
Refrigeración (kW)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia['Refrigeración (kW)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 200000 y 1300000, haciendo que la mayor parte de Refrigeración (kW) esté entre la parte baja y casi superior que del histograma, haciendo haya una cola hacia la derecha y por lo tanto, un sesgo positivo, que implica que no es una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a ver los componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia['Refrigeración (kW)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que parece haber una tendencia al principio ascedente y luego, claramente descendente. Parece haber estacionalidad anual y el ruido parece tener variabilidad constante y estar centrado en 0. También parece haber bastantes datos ruidosos.
Con periodo anual podemos ver perfectamente la tendencia, la estacionalidad y el ruido.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad anual, podemos ver un ascenso desde 2014 hasta 2018, luego un descenso hasta 2020, se mantiene hasta 2021, y luego baja.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver como hay una clara estacionalidad anual, haciéndonos saber que seguramente el patrón se repetirá periódicamente.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, podemos ver que en el ruido no tenemos tendencia, ni estacionalidad, ni outliers, ronda por 0 y hay un ruido más o menos estable al principio y final.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia['Refrigeración (kW)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -3.976635 p-value: 0.001538 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El test nos da un valor 0.001538 < 0.05, por lo que la serie es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Refrigeración (kW)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Parece haber una autocorrelación bastante mayor que 75%, por lo tanto, hay una fuerte dependencia entre valores pasados y presentes para la semana anterior al dato actual.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Refrigeración (kW)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que hasta poco más de un mes (30 días), la autocorrelación sigue siendo alta. A partir de la mitad del año (180 días aprox.), existe una autorrelación negativa destacable. Esto puede confirmarnos, tal y como se puede ver en las gráficas, que el valor de la serie durante la mitad del año baja con la misma fuerza con la que subió con durante la otra mitad. Con lag=365 (un año) confirmamos también que hay mucha autocorrelación y por tanto, que hay estacionalidad.
Calefacción (kWh)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia['Calefacción (kWh)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 30000 y 50000, haciendo que la mayor parte de Calefacción (kWh) esté en la parte baja histograma, haciendo que haya una cola hacia la derecha y por lo tanto, un sesgo positivo, que implica que no es una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante estrecha, puesto que los datos están muy concentrados en una parte pequeña de la variable.
Vamos a revisar las componentes principales de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia['Calefacción (kWh)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que parece haber una tendencia más bien descendente. Parece haber estacionalidad anual y el ruido parece tener variabilidad constante y estar centrado en 0.
Con periodo anual podemos ver cómo se puede presenciar de manera clara la tendencia, estacionalidad y ruido.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, podemos ver cómo hay un ascenso desde 2014 hasta 2015, luego disminuye hasta 2017, aumenta de nuevo muy ligeramente hasta 2019, y luego baja bastante.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Se puede ver cómo hay una clara estacionalidad anual, haciéndonos saber que seguramente el patrón se repetirá en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, podemos ver en cuanto al ruido, que no hay estacionalidad, no hay una tendencia, la gráfica ronda el 0, no hay outliers, y hay poco ruido a diferencia de otras variables, excepto al principio y al final.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia['Calefacción (kWh)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -4.650606 p-value: 0.000104 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El teste de Fuller nos da un p-valor de 0.000104 < 0.05, por lo que la serie es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Calefacción (kWh)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
En cuanto a la autocorrelación podemos ver como supera el 75%, por lo tanto hay una fuerte dependencia entre valores pasados y presentes para 7 días. Destacar también que cuanto mayor es el lag, parece que va decrementando el valor de autocorrelación.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Calefacción (kWh)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que para 30 días, la autocorrelación sigue siendo alta. A partir de la mitad del año, existe una autorrelación negativa apreciable. Con lag=365 (un año) confirmamos también que hay bastante autocorrelación y por tanto, que hay estacionalidad.
Emisión (kg CO₂)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia['Emisión (kg CO₂)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 450000 y 130000, haciendo que la mayor parte de Emisión (kg CO₂) esté entre la parte baja y casi superior del histograma, haciendo que haya una cola hacia la derecha y por lo tanto, un sesgo positivo, que implica que no es una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a revisar las componentes principales de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia['Emisión (kg CO₂)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que parece haber una tendencia al principio ascedente y luego, claramente descendente. Parece haber estacionalidad anual y el ruido parece tener variabilidad constante y estar centrado en 0. Parece que en esta serie también hay bastantes datos ruidosos.
Con periodo anual podemos ver claramente la tendencia, estacionalidad y ruido de la serie temporal de la variable.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Incrementa desde 2014 hasta 2018, y luego a partir de ahí sólo hay decremento.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Se puede ver que hay una clara estacionalidad anual en la serie temporal de la variable, por lo tanto, seguramente habrá una repetición del patrón en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
En cuanto al ruido no vemos estacionalidad, ni tendencia, la gráfica ronda por el 0, no hay outliers y hay más ruido al principio y en el centro de la serie que al final.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia['Emisión (kg CO₂)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -3.987486 p-value: 0.001477 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El estadístico nos da un p-valor de 0.001477 < 0.05, por lo que se rechaza la hipótesis nula. Por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Emisión (kg CO₂)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Se puede ver en la autocorrelación, que todos los lags tienen una autocorrelación por encima del 80%, por lo tanto, hay una fuerte dependencia entre valores pasados y presentes para 7 días. Es decir, un dato de la serie está muy correlacionado con los datos de la semana anterior.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia['Emisión (kg CO₂)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que en un mes, la autocorrelación sigue siendo alta. A partir de la mitad del año (180 días aprox.), existe una autorrelación negativa destacable. Esto puede confirmarnos, tal y como se puede ver en las gráficas, que el valor de la serie durante la mitad del año baja con la misma fuerza con la que subió con durante la otra mitad. Con un año de lag confirmamos también que hay mucha autocorrelación y por tanto, que hay estacionalidad.
Variables endógenas y exógenas¶
En cuanto a las variables endógenas, variables que influyen en la variable objetivo, son las variables: Electricidad (kW), Fotovoltaica (kW), Refrigeración (kW), Calefacción (kWh) y Emisión (kg CO₂).
En cambio las variables exógenas, que son variables que no dependen de las endógenas pero que de las que depende la variable de salida, y por lo tanto, son como variables externas al dataset pero que afectan a su funcionamiento, tenemos la variable Día de la semana.
Guardar dataset¶
Procedemos a ver cómo ha quedado el dataset ya preprocesado.
energia.head()
| Día de la semana | Electricidad (kW) | Fotovoltaica (kW) | Refrigeración (kW) | Calefacción (kWh) | Energía total (kWh) | Emisión (kg CO₂) | |
|---|---|---|---|---|---|---|---|
| 2014-01-01 | 2 | 490049.28 | 62795.47 | 243369.71770 | 20107.60131 | 753526.59901 | 469960.0 |
| 2014-01-02 | 3 | 556486.56 | 64284.00 | 323886.05169 | 23987.86135 | 904360.47304 | 564020.0 |
| 2014-01-03 | 4 | 545831.45 | 67187.06 | 337004.60237 | 22112.20695 | 904948.25932 | 564390.0 |
| 2014-01-04 | 5 | 511495.56 | 46798.28 | 252493.06189 | 21074.73561 | 785063.35750 | 489640.0 |
| 2014-01-05 | 6 | 502806.80 | 70162.87 | 249265.37031 | 22777.47812 | 774849.64843 | 483270.0 |
Una vez terminado el preprocesamiento del dataset de los datos de producción y consumo de energía, procedemos a guardarlo en otro archivo. Así podremos usar los datos preprocesados en los siguientes notebooks.
# Descomentar para volver a guardar el dataset preprocesado
# energia.to_csv('Data/energia_preprocesada.csv')
Datos Climáticos¶
Ahora, vamos a analizar el dataset que contiene los datos climáticos del barrio en cuestión, tras haberlo ya analizado superficialmente en la sección de "Análisis superficial de los datasets".
Fechas perdidas¶
Primero, comprobamos si tenemos fechas perdidas
# Se calcula el numero de fechas desde a través de las hora
fechas_esperadas = pd.date_range(start=climaticos.index.min(), end=climaticos.index.max(), freq='h')
# Diferencias entre la cantidad de horas que se supone que tenemos, y lo que realmente tenemos
fechas_perdidas = fechas_esperadas.difference(climaticos.index)
# Imprimir el número de horas perdidas
print(f'Número de fechas faltantes: {len(fechas_perdidas)}')
# Muestra las horas pérdidas
fechas_perdidas
Número de fechas faltantes: 0
DatetimeIndex([], dtype='datetime64[ns]', freq='h')
Vemos que no tenemos fechas perdidas, por lo que no realizamos nada.
Tratamiento de duplicados¶
Primero analizaremos si hay duplicados.
# Visualizamos la cantidad de duplicados del dataset
climaticos.duplicated().sum()
np.int64(403)
Vemos que tenemos 403 valores duplicados, pero no lo eliminamos porque, del total de 78888 valores que contiene nuestro dataset, consideramos que es factible que puedan existir valores duplicados a lo largo de todas las horas de todos los años incluidos. Por tanto, consideramos que no son valores erróneos y no los eliminamos.
climaticos.index.duplicated().sum()
np.int64(0)
Vemos que no tenemos índices duplicados, por lo que no hay dos días con índices repetidos y no hacemos nada.
Tratamiento de nulos¶
Procedemos a tratar los valores nulos.
# Visualizamos la cantidad de nulos que tiene el dataset
climaticos.isna().sum()
Día de la semana 0 Temperatura (°C) 187 Punto de rocío (°C) 187 Humedad (%) 187 Dirección del viento 533 Velocidad del viento (m/s) 187 Ráfaga de viento (m/s) 187 Presión (hPa) 187 Precipitación (mm) 187 Condición meteorológica 193 dtype: int64
Como se puede ver hay pocos nulos: 533 en Dirección del viento, 193 en Condición meteorológica, y 187 nulos en Temperatura (°C), Punto de rocío (°C), Humedad (%), Velocidad del viento (m/s), Ráfaga de viento (m/s), Presión (hPa) y Precipitación (mm). Por lo tanto, podríamos eliminarlos o imputarlos, en nuestro caso, como no queremos eliminar ya que no queremos perder datos, decidimos imputar, para ello analizamos cómo funciona el dataset.
# Visualizamos como quedan las 5 primeras columnas del dataset
climaticos.head()
| Día de la semana | Temperatura (°C) | Punto de rocío (°C) | Humedad (%) | Dirección del viento | Velocidad del viento (m/s) | Ráfaga de viento (m/s) | Presión (hPa) | Precipitación (mm) | Condición meteorológica | |
|---|---|---|---|---|---|---|---|---|---|---|
| 2014-01-01 00:00:00 | 2 | 11.666667 | -17.777778 | 45.0 | CALMA | 0.00000 | 0.0 | 977.312154 | 0.0 | Despejado |
| 2014-01-01 01:00:00 | 2 | 10.555556 | 0.555556 | 50.0 | E | 2.23520 | 0.0 | 977.312154 | 0.0 | Despejado |
| 2014-01-01 02:00:00 | 2 | 10.000000 | -17.777778 | 50.0 | ESE | 1.34112 | 0.0 | 977.312154 | 0.0 | Despejado |
| 2014-01-01 03:00:00 | 2 | 9.444444 | -17.777778 | 52.0 | ESE | 2.68224 | 0.0 | 977.312154 | 0.0 | Despejado |
| 2014-01-01 04:00:00 | 2 | 9.444444 | -17.777778 | 52.0 | CALMA | 0.00000 | 0.0 | 977.650793 | 0.0 | Despejado |
Imputación mediante interpolación temporal¶
En nuestro dataset, nuestros datos están organizados por horas, por lo que pensamos aprovecharnos de esta característica. La interpolación temporal utiliza el DatetimeIndex para estimar los valores faltantes teniendo en cuenta la distancia real entre timestamps (en este caso, horas). Viendo la documentación, vemos cómo funciona en datos diarios o con mayor resolución, por lo que es adecuado para series horarias como esta. Además, al haber pocos valores nulos, la interpolación temporal proporciona una aproximación suave y coherente sin distorsionar el comportamiento general de la serie.
# Seleccionamos todas las columnas numéricas del dataset
numericas = climaticos.select_dtypes(include='number').columns
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
climaticos[numericas] = climaticos[numericas].interpolate(method='time')
Como este método sólo se puede usar en columnas numéricas, seleccionamos esas columnas y realizamos la operación, dejándonos sólo con valores nulos en las dos variables de tipo object, en las cuales debemos utilizar un método diferente.
# Visualizamos la cantidad de nulos que tiene el dataset
climaticos.isna().sum()
Día de la semana 0 Temperatura (°C) 0 Punto de rocío (°C) 0 Humedad (%) 0 Dirección del viento 533 Velocidad del viento (m/s) 0 Ráfaga de viento (m/s) 0 Presión (hPa) 0 Precipitación (mm) 0 Condición meteorológica 193 dtype: int64
Podemos ver que aún hay 533 nulos en Dirección del viento y 193 nulos en Condición meteorológica. Esto sucede porque la interpolación temporal sólo funciona con variables numéricas, por lo tanto, para tratar a los objects utilizaremos los métodos ffill() y bfill().
Imputación mediante último y siguiente valor observado.¶
Para los nulos en las variables "object" hemos decidido utilizar los métodos ffill() y bfill(), que cogen el valor anterior o posterior respectivamente. Pensamos en utilizarlos juntos por si, tras aplicar uno de ellos, sigue habiendo nulos en ese dato, según cómo estén estos distribuidos en nuestro dataset. Además, la dirección del viento y la condición meteorológica no son variables tan volubles que cambien fácilmente cada hora, por lo que pensamos que son los métodos adecuados para estas dos variables.
# Rellenamos las columnas con el valor anterior y posterior (ffill y bfill respectivamente)
climaticos['Dirección del viento'] = climaticos['Dirección del viento'].ffill().bfill()
climaticos['Condición meteorológica'] = climaticos['Condición meteorológica'].ffill().bfill()
Con esto podemos observar cómo hemos eliminado todos los nulos de nuestro dataset.
# Visualizamos la cantidad de nulos que tiene el dataset
climaticos.isna().sum()
Día de la semana 0 Temperatura (°C) 0 Punto de rocío (°C) 0 Humedad (%) 0 Dirección del viento 0 Velocidad del viento (m/s) 0 Ráfaga de viento (m/s) 0 Presión (hPa) 0 Precipitación (mm) 0 Condición meteorológica 0 dtype: int64
Vemos que efectivamente ya no tenemos valores missing por lo que pasamos a tratar los datos atípicos.
Outliers¶
Vamos a analizar si hay valores sospechosos en cada una de las variables. Primero, analizaremos los 5 primeros datos de nuestro dataset.
# Visualizamos las 5 primeras columnas del dataset
climaticos.head()
| Día de la semana | Temperatura (°C) | Punto de rocío (°C) | Humedad (%) | Dirección del viento | Velocidad del viento (m/s) | Ráfaga de viento (m/s) | Presión (hPa) | Precipitación (mm) | Condición meteorológica | |
|---|---|---|---|---|---|---|---|---|---|---|
| 2014-01-01 00:00:00 | 2 | 11.666667 | -17.777778 | 45.0 | CALMA | 0.00000 | 0.0 | 977.312154 | 0.0 | Despejado |
| 2014-01-01 01:00:00 | 2 | 10.555556 | 0.555556 | 50.0 | E | 2.23520 | 0.0 | 977.312154 | 0.0 | Despejado |
| 2014-01-01 02:00:00 | 2 | 10.000000 | -17.777778 | 50.0 | ESE | 1.34112 | 0.0 | 977.312154 | 0.0 | Despejado |
| 2014-01-01 03:00:00 | 2 | 9.444444 | -17.777778 | 52.0 | ESE | 2.68224 | 0.0 | 977.312154 | 0.0 | Despejado |
| 2014-01-01 04:00:00 | 2 | 9.444444 | -17.777778 | 52.0 | CALMA | 0.00000 | 0.0 | 977.650793 | 0.0 | Despejado |
De momento no parece haber nada raro, vamos a analizar con más profundidad observando las series temporales de cada variable.
# Guarda el número de variables del dataset
n = len(climaticos.columns)
# Creamos las figuras y ejes con 1 columna que cada fila tendrá una variable, en figsize poniendo anchura y altura, comparten el mismo eje x
fig, axes = plt.subplots(nrows=n, ncols=1, figsize=(14, max(2, 1.6 * n)), sharex=True)
# Dibuja la serie temporal de cada columna, pone su nombre al eje y y coloca un grid
for ax, col in zip(axes, climaticos.columns):
ax.plot(climaticos.index, climaticos[col], linewidth=1)
ax.set_ylabel(col)
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Coloca en la última gráfica el eje X
axes[-1].set_xlabel("Tiempo")
# Nombre general
fig.suptitle("Series temporales del barrio — cada serie en su propia fila", y=0.995)
# Distancia entre filas
plt.tight_layout(rect=[0, 0, 1, 0.99])
# Se muestra la gráfica
plt.show()
Intentamos analizar lo que vemos a simple vista en cada variable:
Temperatura (ºC): la temperatura suele estar entre 0 y 40 y pocos, con bajadas de golpe inferiores a -15 ºC. Creemos que existen outliers.
Punto de rocío (ºC): se mueve alrededor de -20 y 23 ºC. En algunos puntos la variable actúa de forma extraña, dando bajones en zona muy altas, por lo que indica que también hay presencia de outliers.
Humedad (%): a simple vista no parece que haya ningún valor por debajo de 0 o mayor a 100, por lo que no consideramos que hayan outliers.
Dirección del viento, al ser una variable categórica, no se nota la presencia de outliers.
Velocidad del viento (m/s): va de 0 a 15 con picos de hasta 22 (alrededor) en algunos puntos, aunque hay algunos picos muy altos. Por el contexto de la variable consideramos que no existen outliers.
Rafaga de viento (m/s): va desde 0 hasta 20, con picos de 23 o alrededor. Se considera que no hay outliers viendo el contexto de la variable y la información de la variable anterior.
Presion (hPa): suele estar fija en 1000, con 2 bajones a 0. Por lo tanto existen outliers.
Precipitacion (mm): aunque suele estar en 0, tiene picos muy altos de hasta 20 o más. Leyendo el contexto de la variable, no consideramos que existan outliers.
Condicion meteorologica:, como sucedia en direccion del viento, al ser una variable categórica, no contiene outliers.
Temperatura (°C)¶
Vamos a analizar la variable de temperatura.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(climaticos['Temperatura (°C)'].describe())
count 78888.000000 mean 24.785825 std 9.512069 min -17.777778 25% 17.222222 50% 25.000000 75% 32.222222 max 48.333333 Name: Temperatura (°C), dtype: float64
Aquí parece ser normal, puesto que hayan temperaturas entre -18ºC y 49ºC puede ser perfectamente correcto. Por lo tanto, para esta variable, analizaremos los outliers desde su gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Temperatura (°C)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Temperatura (°C)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Temperatura (°C)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Aquí se destacan bastante los outliers, puesto que la forma de la gráfica no suele bajar demasiado de 0 (o incluso parece que -1ºC). Por lo tanto, solo detectaremos como atípicos los valores que tengan menos de -10ºC, puesto que son picos muy notorios hacia abajo. Estas temperaturas no tienen sentido sobre la zona donde analizamos el dataset del problema. Probablemente se traten de errores de medición.
# Elimina todos los valores por debajo de -10ºC
climaticos['Temperatura (°C)'] = climaticos['Temperatura (°C)'].where((climaticos['Temperatura (°C)'] >= -10)
)
Comprobamos cuántos outliers hemos eliminado.
# Visualizamos la cantidad de nulos que tiene el dataset
climaticos.isna().sum()
Día de la semana 0 Temperatura (°C) 15 Punto de rocío (°C) 0 Humedad (%) 0 Dirección del viento 0 Velocidad del viento (m/s) 0 Ráfaga de viento (m/s) 0 Presión (hPa) 0 Precipitación (mm) 0 Condición meteorológica 0 dtype: int64
Vemos que hemos eliminado 15 outliers. Vamos a imputar con intepolación temporal
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
climaticos['Temperatura (°C)'] = climaticos['Temperatura (°C)'].interpolate(method='time')
Vemos cómo queda el dataset.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
climaticos['Temperatura (°C)'].describe()
count 78888.000000 mean 24.793688 std 9.494418 min -1.111111 25% 17.222222 50% 25.000000 75% 32.222222 max 48.333333 Name: Temperatura (°C), dtype: float64
Hemos imputado todos los nulos y sus valores parecen haber quedado bien. Tienen un mínimo de -1.11, un máximo de 48.33, una mediana de 25, y una desviación típica de 9.5.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Temperatura (°C)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Temperatura (°C)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Temperatura (°C)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Podemos ver que aún hay un outlier que baja demasiado, por lo tanto vamos a eliminarlo de la misma manera, pero esta vez vamos a poner todo lo que sea menos de -2 grados.
# Elimina todos los valores por debajo de -2ºC
climaticos['Temperatura (°C)'] = climaticos['Temperatura (°C)'].where((climaticos['Temperatura (°C)'] >= -2)
)
Vamos a comprobar que lo hemos eliminado.
# Visualizamos la cantidad de nulos que tiene el dataset
climaticos.isna().sum()
Día de la semana 0 Temperatura (°C) 0 Punto de rocío (°C) 0 Humedad (%) 0 Dirección del viento 0 Velocidad del viento (m/s) 0 Ráfaga de viento (m/s) 0 Presión (hPa) 0 Precipitación (mm) 0 Condición meteorológica 0 dtype: int64
Efectivamente, lo hemos eliminado poniendolo a nulo.
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
climaticos['Temperatura (°C)'] = climaticos['Temperatura (°C)'].interpolate(method='time')
Realizamos el interpolado temrporal.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
climaticos['Temperatura (°C)'].describe()
count 78888.000000 mean 24.793688 std 9.494418 min -1.111111 25% 17.222222 50% 25.000000 75% 32.222222 max 48.333333 Name: Temperatura (°C), dtype: float64
Los nulos han sido tratados.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Temperatura (°C)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Temperatura (°C)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Temperatura (°C)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Hemos logrado eliminar los valores atípicos.
Vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=climaticos.index.month, y='Temperatura (°C)', data=climaticos)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Temperatura (°C)')
plt.title('Boxplot mensual de Temperatura (°C)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver un lógico aumento de temperatura a partir de febrero hasta alcanzar sus máximos en junio, julio y agosto, y como decrementa a partir de este mes hasta llegar a enero.
Punto de rocío (°C)¶
Vamos a analizar esta variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(climaticos['Punto de rocío (°C)'].describe())
count 78888.000000 mean 3.794071 std 8.246256 min -22.777778 25% -2.222222 50% 2.777778 75% 10.000000 max 24.444444 Name: Punto de rocío (°C), dtype: float64
Vemos como los valores de esta variable parecen normales. Tiene un mínimo de -22.78, un máximo de 24.44, una mediana de 2.78 y una desviación típica de 8.25.
Vamos a analizar la gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Punto de rocío (°C)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Punto de rocío (°C)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Punto de rocío (°C)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Vemos que aunque haya algun bajón, parece que es el funcionamiento típico de la variable. Por lo tanto, concluimos que en esta variable no parece haber outliers.
Vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=climaticos.index.month, y='Punto de rocío (°C)', data=climaticos)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Punto de rocío (°C)')
plt.title('Boxplot mensual de Punto de rocío (°C)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver que el punto de rocío alcanza sus máximos en julio, agosto, septiembre y octubre.
Humedad (%)¶
Vamos a analizar esta variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(climaticos['Humedad (%)'].describe())
count 78888.000000 mean 31.297536 std 19.549718 min 0.000000 25% 17.000000 50% 27.000000 75% 41.000000 max 100.000000 Name: Humedad (%), dtype: float64
Vemos que los valores de humedad de forma porcentual son lógicos. Tiene un mínimo de 0, un máximo del 100, una mediana de 27 y una desviación típica de 20.
Vamos a visualizar la gráfica de su serie temporal.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Humedad (%)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Humedad (%)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Humedad (%)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Vemos que en esta serie temporal, tampoco parece haber outliers, ya que no se aprecia ningún comportamiento atípico de la misma en ningún punto.
Vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=climaticos.index.month, y='Humedad (%)', data=climaticos)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Humedad (%)')
plt.title('Boxplot mensual de Humedad (%)')
plt.show()
En esta serie parece haber bastante más outliers, pero como estamos midiendo el porcentaje de humedad sobre meses en distintos años, las medidas son muy variables, por lo que consideramos que estos datos pueden ser normales y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver que el incremento de humedad en diciembre y enero.
Velocidad del viento (m/s)¶
Vamos a analizar esta variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(climaticos['Velocidad del viento (m/s)'].describe())
count 78888.000000 mean 2.884915 std 1.964542 min 0.000000 25% 1.341120 50% 2.682240 75% 3.576320 max 21.010880 Name: Velocidad del viento (m/s), dtype: float64
Se puede ver, que aunque por encima del 75% se dispara, no parece valores outliers. tiene un mínimo de 0, un máximo de 21, una mediana de 2.68224 y una desviación típica de 2.
Vamos a analizar su serie temporal en la gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Velocidad del viento (m/s)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Velocidad del viento (m/s)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Velocidad del viento (m/s)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Aquí se ve que aunque hay subidas muy altas de viento, puede ser lógicas debido a borrascas y rachas de viento puntuales en un día principalmente (contexto de la variable), por lo que consideramos que no contiene outliers. Parece razonable pensar que es posible que en ciertas horas de ciertos días concretos se puedan producir fuertes rachas de viento.
Vamos a ver los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=climaticos.index.month, y='Velocidad del viento (m/s)', data=climaticos)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Velocidad del viento (m/s)')
plt.title('Boxplot mensual de Velocidad del viento (m/s)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver que alcanza sus máximos en el mes de agosto.
Ráfaga de viento (m/s)¶
Vamos a analizar esta variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(climaticos['Ráfaga de viento (m/s)'].describe())
count 78888.000000 mean 0.650483 std 2.574001 min 0.000000 25% 0.000000 50% 0.000000 75% 0.000000 max 28.163520 Name: Ráfaga de viento (m/s), dtype: float64
Puesto que la propia variable indica que son ráfagas, se puede ver que el mínimo, la mediana y el 75% son 0, tiene un máximo lógico de 28.16, y una desviación típica de 2.57.
Vamos a analizar su serie temporal en una gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Ráfaga de viento (m/s)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Ráfaga de viento (m/s)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Ráfaga de viento (m/s)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Cómo hemos descrito antes, al ser ráfagas, los valores estarán en 0 y habrán picos hacia arriba, que encima no son exagerados, por lo tanto lo consideramos normales dentro del contexto de la variable y decidimos no eliminar nada, puesto que no vemos outliers.
Ahora vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=climaticos.index.month, y='Ráfaga de viento (m/s)', data=climaticos)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Ráfaga de viento (m/s)')
plt.title('Boxplot mensual de Ráfaga de viento (m/s)')
plt.show()
Por el funcionamiento de la variable, no consideramos que existan outliers. Ya que lo normal es que no hayan ráfagas de viento. Parece razonable pensar que lo importante de esta variable son los datos que se consideran atípicos, porque son los que correspoden con fuertes ráfagas de viento momentáneas.
Presión (hPa)¶
Vamos a analizar la variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(climaticos['Presión (hPa)'].describe())
count 78888.000000 mean 972.419750 std 7.531601 min 0.000000 25% 969.184818 50% 971.893930 75% 975.280320 max 991.534992 Name: Presión (hPa), dtype: float64
Como estamos midiendo la presión atmosférica sobre una zona geográfica concreta, lo normal es que no cambie demasiado. Por ello los valores de la mediana y el máximo sean parecidos (971.89 y 991.53 respectivamente). La desviación típica es pequeña, de 7.53, por lo que la mayoría de los datos suelen estar entre estos valores. Consideramos que el mínimo de 0 es un error de medida, porque no tiene sentido que deje de haber presión sobre la zona. Por lo tanto, decidimos eliminar únicamente los valores que bajen de 900 (hPa) de presión.
# Sustituimos los valores encima del 75% por nulos
climaticos['Presión (hPa)'] = climaticos['Presión (hPa)'].where((climaticos['Presión (hPa)'] >= 900)
)
Revisamos cuántos outliers hemos eliminado.
# Visualizamos la cantidad de nulos que tiene el dataset
climaticos.isna().sum()
Día de la semana 0 Temperatura (°C) 0 Punto de rocío (°C) 0 Humedad (%) 0 Dirección del viento 0 Velocidad del viento (m/s) 0 Ráfaga de viento (m/s) 0 Presión (hPa) 3 Precipitación (mm) 0 Condición meteorológica 0 dtype: int64
Vemos cómo hemos eliminado 3 outliers. Ahora imputaremos a través de interpolación temporal.
# Utilizamos interpolación temporal en el dataset en las columnas seleccionadas
climaticos['Presión (hPa)'] = climaticos['Presión (hPa)'].interpolate(method='time')
Revisamos de nuevo la variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
climaticos['Presión (hPa)'].describe()
count 78888.000000 mean 972.456865 std 4.556768 min 957.332453 25% 969.184818 50% 971.893930 75% 975.280320 max 991.534992 Name: Presión (hPa), dtype: float64
Se puede ver cómo ya no existen nulos ni valores extraños. Tiene un mínimo de 957, un máximo de 992, una mediana de 972 y una desviación típica de 4.56.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Presión (hPa)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Presión (hPa)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Presión (hPa)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Se puede ver en la gráfica como ya se muestra la serie temporal de la variable sin problemas.
Vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=climaticos.index.month, y='Presión (hPa)', data=climaticos)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Presión (hPa)')
plt.title('Boxplot mensual de Presión (hPa)')
plt.show()
Podemos ver que aunque existen outliers son muy ligeros, y por lo tanto, no los tratamos. Debemos recordar que, el método intercuartílico para outliers (valores atípicos), identifica puntos de datos inusualmente altos o bajos usando el Rango Intercuartílico (IQR), calculando los límites inferior como (Q1-1.5 x IQR) y superior como (Q3+1.5 x IQR), donde (Q1) y (Q3) son los cuartiles 1 y 3, respectivamente, y cualquier dato fuera de estos límites se considera un outlier. **En vista del boxplot, los valores que aparecen dibujados como atípicos no parecen alejarse demasiado del rango intercuartílico, por lo que consideramos que pueden tratarse de valores altos que son algo excepcionales, pero que pueden ser normales. Por esto, decidimos no eliminarlos. Sólo quitaremos los *hard outliers***
Además, podemos ver que aumenta en los meses de invierno.
Precipitación (mm)¶
Vamos a analizar esta variable.
# Visualizamos minimo, maximo, media, desviación típica, 25%, 50%, 75% y el número de valores que tiene la variable
print(climaticos['Precipitación (mm)'].describe())
count 78888.000000 mean 0.017226 std 0.331767 min 0.000000 25% 0.000000 50% 0.000000 75% 0.000000 max 25.400000 Name: Precipitación (mm), dtype: float64
Se puede ver cómo gran parte de los valores están en 0, rasgo característico de zonas muy poco lluviosas. Por lo que los valores que están fuera de este rango son valores normales, signo de que hay precipitación, por lo tanto no los consideramos outliers. Tiene un mínimo, mediana y 75% de 0, un máximo de 25.4 y una desviación típica de 0.33.
Ahora vamos a revisar su serie temporal en la gráfica.
# Creamos la gráfica de anchura 14, altura 4
fig, ax = plt.subplots(figsize=(14, 4))
# Pasamos las columnas que queremos representar
ax.plot(climaticos.index, climaticos['Precipitación (mm)'], linewidth=1)
# Nombre de la columna en el eje y
ax.set_ylabel('Precipitación (mm)')
# Nombre de la columna en el eje x
ax.set_xlabel('Tiempo')
# Nombre de la grafica
ax.set_title('Serie temporal de Precipitación (mm)')
# Grid de la gráfica
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Imprime la gráfica
plt.show()
Aquí se puede ver cómo efectivamente, su serie temporal demuestra el tiempo donde ha habido lluvia con precipitaciones dentro de valores razonables, por lo tanto no consideramos que esta variable tenga outliers.
Ahora vamos a analizar los boxplot de la variable.
# Se crera una figura (10, 5)
plt.figure(figsize=(10, 5))
# Boxplot con el eje X los meses, e y la columna que queremos analizar
sns.boxplot(x=climaticos.index.month, y='Precipitación (mm)', data=climaticos)
# Creamos la gráfica
plt.xlabel('Mes')
plt.ylabel('Precipitación (mm)')
plt.title('Boxplot mensual de Precipitación (mm)')
plt.show()
Al igual como sucede con la variable Ráfaga de viento (m/s), no consideramos que hayan outliers porque este es el funcionamiento de la variable, ya que normalmente no va a llover, por lo que cada vez que haya lluvia lo trata como outliers aunque no lo sean. Consideramos que lo más importante de esta variable son los datos atípicos, puesto que son los que marcan los días lluviosos.
Transformación de variables categóricas a numéricas¶
Transformaremos "Dirección del viento" y "Condición meteorológica" de object a numéricas, ya que nos resultarán mucho más útil para nuestros modelos que estén en numéricas.
#Seleccionamos las columnas categóricas y las mostramos
categoricas = climaticos.select_dtypes(include = ["object"]).columns
categoricas
Index(['Dirección del viento', 'Condición meteorológica'], dtype='object')
Seleccionamos las variables categoricas.
#Transformamos las categóricas a dummies, eliminando la primera columna de cada variable categórica, haciendo que desaparezca la colinealidad, combinación lineal de varias columnas para formar otra.
categoricas_Datos = pd.get_dummies(climaticos[categoricas], drop_first = True)
# Mostramos la transformación de las variables categóricas en variables dummies
categoricas_Datos.head()
| Dirección del viento_E | Dirección del viento_ENE | Dirección del viento_ESE | Dirección del viento_N | Dirección del viento_NE | Dirección del viento_NNE | Dirección del viento_NNO | Dirección del viento_NO | Dirección del viento_O | Dirección del viento_ONO | ... | Condición meteorológica_Neblina | Condición meteorológica_Niebla | Condición meteorológica_Nublado | Condición meteorológica_Parcialmente_nublado | Condición meteorológica_Patches | Condición meteorológica_Smoke | Condición meteorológica_Squalls | Condición meteorológica_T-Storm | Condición meteorológica_Thunder | Condición meteorológica_Widespread | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2014-01-01 00:00:00 | False | False | False | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2014-01-01 01:00:00 | True | False | False | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2014-01-01 02:00:00 | False | False | True | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2014-01-01 03:00:00 | False | False | True | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2014-01-01 04:00:00 | False | False | False | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
5 rows × 33 columns
Creamos las dummies de las variables categóricas, eliminando la primera columna.
# Eliminamos los atributos originales
climaticos.drop(categoricas, axis = 1, inplace = True)
# Concatenamos los nuevos atributos creados para las variables categóricas
climaticos = pd.concat([climaticos, categoricas_Datos], axis = 1)
# Mostramos como quedan los datos de nuevo
climaticos.head()
| Día de la semana | Temperatura (°C) | Punto de rocío (°C) | Humedad (%) | Velocidad del viento (m/s) | Ráfaga de viento (m/s) | Presión (hPa) | Precipitación (mm) | Dirección del viento_E | Dirección del viento_ENE | ... | Condición meteorológica_Neblina | Condición meteorológica_Niebla | Condición meteorológica_Nublado | Condición meteorológica_Parcialmente_nublado | Condición meteorológica_Patches | Condición meteorológica_Smoke | Condición meteorológica_Squalls | Condición meteorológica_T-Storm | Condición meteorológica_Thunder | Condición meteorológica_Widespread | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2014-01-01 00:00:00 | 2 | 11.666667 | -17.777778 | 45.0 | 0.00000 | 0.0 | 977.312154 | 0.0 | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2014-01-01 01:00:00 | 2 | 10.555556 | 0.555556 | 50.0 | 2.23520 | 0.0 | 977.312154 | 0.0 | True | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2014-01-01 02:00:00 | 2 | 10.000000 | -17.777778 | 50.0 | 1.34112 | 0.0 | 977.312154 | 0.0 | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2014-01-01 03:00:00 | 2 | 9.444444 | -17.777778 | 52.0 | 2.68224 | 0.0 | 977.312154 | 0.0 | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2014-01-01 04:00:00 | 2 | 9.444444 | -17.777778 | 52.0 | 0.00000 | 0.0 | 977.650793 | 0.0 | False | False | ... | False | False | False | False | False | False | False | False | False | False |
5 rows × 41 columns
Revisamos que, efectivamente, tenemos todas las columnas juntas.
Unir datasets¶
Puesto que la variable objetivo es Energia_total, que pertenece al otro dataset, uniremos ambos ya que no resultará útil para medir las correlaciones entre variables.
# Unimos dos datasets con el mismo DatetimeIndex, eliminando una columna compartida
energia_climaticos = energia_horas.join(climaticos.drop(columns='Día de la semana'), how='outer')
# Información del dataset
energia_climaticos.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 78888 entries, 2014-01-01 00:00:00 to 2022-12-31 23:00:00 Data columns (total 47 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Día de la semana 78888 non-null int64 1 Electricidad (kW) 78888 non-null float64 2 Fotovoltaica (kW) 78888 non-null float64 3 Refrigeración (kW) 78888 non-null float64 4 Calefacción (kWh) 78888 non-null float64 5 Energía total (kWh) 78888 non-null float64 6 Emisión (kg CO₂) 78888 non-null float64 7 Temperatura (°C) 78888 non-null float64 8 Punto de rocío (°C) 78888 non-null float64 9 Humedad (%) 78888 non-null float64 10 Velocidad del viento (m/s) 78888 non-null float64 11 Ráfaga de viento (m/s) 78888 non-null float64 12 Presión (hPa) 78888 non-null float64 13 Precipitación (mm) 78888 non-null float64 14 Dirección del viento_E 78888 non-null bool 15 Dirección del viento_ENE 78888 non-null bool 16 Dirección del viento_ESE 78888 non-null bool 17 Dirección del viento_N 78888 non-null bool 18 Dirección del viento_NE 78888 non-null bool 19 Dirección del viento_NNE 78888 non-null bool 20 Dirección del viento_NNO 78888 non-null bool 21 Dirección del viento_NO 78888 non-null bool 22 Dirección del viento_O 78888 non-null bool 23 Dirección del viento_ONO 78888 non-null bool 24 Dirección del viento_OSO 78888 non-null bool 25 Dirección del viento_S 78888 non-null bool 26 Dirección del viento_SE 78888 non-null bool 27 Dirección del viento_SO 78888 non-null bool 28 Dirección del viento_SSE 78888 non-null bool 29 Dirección del viento_SSO 78888 non-null bool 30 Dirección del viento_VAR 78888 non-null bool 31 Condición meteorológica_Despejado 78888 non-null bool 32 Condición meteorológica_Duststorm 78888 non-null bool 33 Condición meteorológica_Heavy 78888 non-null bool 34 Condición meteorológica_Light 78888 non-null bool 35 Condición meteorológica_Lluvia 78888 non-null bool 36 Condición meteorológica_Mayormente_nublado 78888 non-null bool 37 Condición meteorológica_Neblina 78888 non-null bool 38 Condición meteorológica_Niebla 78888 non-null bool 39 Condición meteorológica_Nublado 78888 non-null bool 40 Condición meteorológica_Parcialmente_nublado 78888 non-null bool 41 Condición meteorológica_Patches 78888 non-null bool 42 Condición meteorológica_Smoke 78888 non-null bool 43 Condición meteorológica_Squalls 78888 non-null bool 44 Condición meteorológica_T-Storm 78888 non-null bool 45 Condición meteorológica_Thunder 78888 non-null bool 46 Condición meteorológica_Widespread 78888 non-null bool dtypes: bool(33), float64(13), int64(1) memory usage: 13.5 MB
Podemos ver que al juntar las 7 columnas del dataset de energía y las 40 variables de climáticos (41, pero eliminamos la columna repetida) tenemos un nuevo dataset, el cual, es muy grande, así que vamos a mirar sus correlaciones para ir eliminando variables (excepto "Día de la semana").
Tabla de correlación¶
Ahora vamos a estudiar las correlaciones del dataset.
En primer lugar vamos a revisar la varianza.
# Calcular varianza de todas las columnas numéricas
varianza = energia_climaticos.var()
varianza
Día de la semana 3.997008e+00 Electricidad (kW) 2.944654e+07 Fotovoltaica (kW) 2.799875e+07 Refrigeración (kW) 2.673083e+08 Calefacción (kWh) 1.079491e+06 Energía total (kWh) 4.179979e+08 Emisión (kg CO₂) 1.624104e+08 Temperatura (°C) 9.014397e+01 Punto de rocío (°C) 6.800073e+01 Humedad (%) 3.821915e+02 Velocidad del viento (m/s) 3.859426e+00 Ráfaga de viento (m/s) 6.625483e+00 Presión (hPa) 2.076413e+01 Precipitación (mm) 1.100692e-01 Dirección del viento_E 1.422453e-01 Dirección del viento_ENE 3.332627e-02 Dirección del viento_ESE 8.285681e-02 Dirección del viento_N 1.721183e-02 Dirección del viento_NE 1.317003e-02 Dirección del viento_NNE 9.242757e-03 Dirección del viento_NNO 1.800624e-02 Dirección del viento_NO 2.834140e-02 Dirección del viento_O 9.787474e-02 Dirección del viento_ONO 4.611321e-02 Dirección del viento_OSO 6.563288e-02 Dirección del viento_S 2.890205e-02 Dirección del viento_SE 4.534549e-02 Dirección del viento_SO 4.745040e-02 Dirección del viento_SSE 2.798317e-02 Dirección del viento_SSO 2.131584e-02 Dirección del viento_VAR 6.124177e-02 Condición meteorológica_Despejado 2.499513e-01 Condición meteorológica_Duststorm 3.802763e-05 Condición meteorológica_Heavy 5.067973e-04 Condición meteorológica_Light 9.976147e-03 Condición meteorológica_Lluvia 1.126923e-03 Condición meteorológica_Mayormente_nublado 1.846760e-01 Condición meteorológica_Neblina 3.421446e-04 Condición meteorológica_Niebla 1.647655e-04 Condición meteorológica_Nublado 6.854958e-02 Condición meteorológica_Parcialmente_nublado 1.432831e-01 Condición meteorológica_Patches 2.407928e-04 Condición meteorológica_Smoke 2.535208e-05 Condición meteorológica_Squalls 3.802763e-05 Condición meteorológica_T-Storm 5.701108e-04 Condición meteorológica_Thunder 1.822064e-03 Condición meteorológica_Widespread 6.337778e-05 dtype: float64
Podemos ver que hay demasiadas columnas y no se puede visualizar bien la varianza en este dataset, por lo tanto, lo tratamos haciendo un filtro de todos las variables que tengan una varianza menor que 1 (próximas a 0).
# Seleccionamos las variables con varianza menor a 1
varianza_baja = varianza[varianza < 1]
# Ordenamos las variables de menor a mayor
varianza_baja = varianza_baja.sort_values()
# Imprimimos
varianza_baja
Condición meteorológica_Smoke 0.000025 Condición meteorológica_Squalls 0.000038 Condición meteorológica_Duststorm 0.000038 Condición meteorológica_Widespread 0.000063 Condición meteorológica_Niebla 0.000165 Condición meteorológica_Patches 0.000241 Condición meteorológica_Neblina 0.000342 Condición meteorológica_Heavy 0.000507 Condición meteorológica_T-Storm 0.000570 Condición meteorológica_Lluvia 0.001127 Condición meteorológica_Thunder 0.001822 Dirección del viento_NNE 0.009243 Condición meteorológica_Light 0.009976 Dirección del viento_NE 0.013170 Dirección del viento_N 0.017212 Dirección del viento_NNO 0.018006 Dirección del viento_SSO 0.021316 Dirección del viento_SSE 0.027983 Dirección del viento_NO 0.028341 Dirección del viento_S 0.028902 Dirección del viento_ENE 0.033326 Dirección del viento_SE 0.045345 Dirección del viento_ONO 0.046113 Dirección del viento_SO 0.047450 Dirección del viento_VAR 0.061242 Dirección del viento_OSO 0.065633 Condición meteorológica_Nublado 0.068550 Dirección del viento_ESE 0.082857 Dirección del viento_O 0.097875 Precipitación (mm) 0.110069 Dirección del viento_E 0.142245 Condición meteorológica_Parcialmente_nublado 0.143283 Condición meteorológica_Mayormente_nublado 0.184676 Condición meteorológica_Despejado 0.249951 dtype: float64
Podemos ver las variables que nos salen:
Condición meteorológica_Smoke 0.000025
Condición meteorológica_Squalls 0.000038
Condición meteorológica_Duststorm 0.000038
Condición meteorológica_Widespread 0.000063
Condición meteorológica_Niebla 0.000165
Condición meteorológica_Patches 0.000241
Condición meteorológica_Neblina 0.000342
Condición meteorológica_Heavy 0.000507
Condición meteorológica_T-Storm 0.000570
Condición meteorológica_Lluvia 0.001127
Condición meteorológica_Thunder 0.001835
Dirección del viento_NNE 0.009255
Condición meteorológica_Light 0.009976
Dirección del viento_NE 0.013207
Dirección del viento_N 0.017261
Dirección del viento_NNO 0.018153
Dirección del viento_SSO 0.021473
Dirección del viento_SSE 0.028138
Dirección del viento_NO 0.028437
Dirección del viento_S 0.028962
Dirección del viento_ENE 0.033539
Dirección del viento_SE 0.045621
Dirección del viento_ONO 0.046251
Dirección del viento_SO 0.047736
Dirección del viento_VAR 0.061253
Dirección del viento_OSO 0.065796
Condición meteorológica_Nublado 0.068604
Dirección del viento_ESE 0.083116
Dirección del viento_O 0.098063
Precipitación (mm) 0.110069
Dirección del viento_E 0.142170
Condición meteorológica_Parcialmente_nublado 0.143531
Condición meteorológica_Mayormente_nublado 0.184579
Condición meteorológica_Despejado 0.249947
dtype: float64
La variable con varianza más alta de este filtro es "Condición meteorológica_Despejado" que sigue siendo una variable muy próxima a 0, por lo tanto, eliminaremos todas las variables del listado del filtro. Además, "Dirección del viento" y "Condición meteorológica" no parece que nos puedan aportar mucha información a nuestra variable objetivo de "energía total". La dirección del viento no nos da información sobre la velocidad del mismo y cómo pueda afectar a la producción de energía (eólica p.e.). Condición meteorológica, por su parte, sólo podría influir poco en el consumo energético, como podría ser que se enciendan algunas luces en días nublados o ninguna luz en día despejado, por lo tanto la consideramos una variable muy poco informativa y también la eliminamos. Destacar también que "Día de la semana" tampoco esté en la lista, ya que no queremos eliminarla.
energia_climaticos = energia_climaticos.drop(columns=varianza_baja.index)
energia_climaticos
| Día de la semana | Electricidad (kW) | Fotovoltaica (kW) | Refrigeración (kW) | Calefacción (kWh) | Energía total (kWh) | Emisión (kg CO₂) | Temperatura (°C) | Punto de rocío (°C) | Humedad (%) | Velocidad del viento (m/s) | Ráfaga de viento (m/s) | Presión (hPa) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2014-01-01 00:00:00 | 2 | 21294.33 | 2.39 | 8098.10352 | 876.28229 | 30268.71581 | 18880.0 | 11.666667 | -17.777778 | 45.0 | 0.00000 | 0.0 | 977.312154 |
| 2014-01-01 01:00:00 | 2 | 21190.46 | 1.95 | 7312.22987 | 873.35158 | 29376.04145 | 18320.0 | 10.555556 | 0.555556 | 50.0 | 2.23520 | 0.0 | 977.312154 |
| 2014-01-01 02:00:00 | 2 | 21537.07 | 4.29 | 7305.86410 | 911.45081 | 29754.38491 | 18560.0 | 10.000000 | -17.777778 | 50.0 | 1.34112 | 0.0 | 977.312154 |
| 2014-01-01 03:00:00 | 2 | 21688.66 | 9.65 | 7573.82433 | 870.42087 | 30132.90520 | 18790.0 | 9.444444 | -17.777778 | 52.0 | 2.68224 | 0.0 | 977.312154 |
| 2014-01-01 04:00:00 | 2 | 23044.40 | 10.19 | 7462.96849 | 946.61933 | 31453.98782 | 19620.0 | 9.444444 | -17.777778 | 52.0 | 0.00000 | 0.0 | 977.650793 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2022-12-31 19:00:00 | 5 | 15684.46 | 1.65 | 15884.38982 | 2464.72711 | 34033.57693 | 21220.0 | 16.666667 | 10.000000 | 65.0 | 3.12928 | 0.0 | 972.909847 |
| 2022-12-31 20:00:00 | 5 | 15471.84 | 1.66 | 15171.98630 | 2496.96492 | 33140.79122 | 20670.0 | 15.000000 | 10.000000 | 72.0 | 2.23520 | 0.0 | 972.909847 |
| 2022-12-31 21:00:00 | 5 | 15121.29 | 1.75 | 14711.04828 | 2526.27202 | 32358.61030 | 20180.0 | 15.000000 | 10.000000 | 72.0 | 1.34112 | 0.0 | 972.571208 |
| 2022-12-31 22:00:00 | 5 | 14721.74 | 1.84 | 14050.59085 | 2385.59794 | 31157.92879 | 19430.0 | 14.444444 | 9.444444 | 72.0 | 0.00000 | 0.0 | 972.232569 |
| 2022-12-31 23:00:00 | 5 | 14721.74 | 1.99 | 12404.00179 | 2341.63729 | 29467.37908 | 18150.0 | 13.888889 | 10.000000 | 77.0 | 2.23520 | 0.0 | 971.893930 |
78888 rows × 13 columns
Podemos ver como nos hemos quedado con 13 variables de 47 que teníamos, por lo tanto, nos hemos desecho de 34 variables, de las cuales destacamos todas las variable dummies de "Dirección del viento" y "Condición meteorológica". También nos hemos desecho de otra variable que no aporta mucho al dataset por su pequeña varianza: Precipitación (mm).
Ahora revisamos qué variables tienen más correlación con la variable objetivo "Energía total (kWh)"
# Hacemos una matriz de correlación y visualizaremos sobre nuestra columna objetivo cuáles columnas parecen ser la más influyentes
matriz_correlacion = energia_climaticos.corr()
# Visualizaremos de mayor a menor
matriz_correlacion["Energía total (kWh)"].sort_values(ascending=False)
Energía total (kWh) 1.000000 Emisión (kg CO₂) 0.998728 Refrigeración (kW) 0.985781 Electricidad (kW) 0.899219 Temperatura (°C) 0.843360 Punto de rocío (°C) 0.564681 Fotovoltaica (kW) 0.347314 Velocidad del viento (m/s) 0.178105 Ráfaga de viento (m/s) 0.127897 Día de la semana -0.079916 Humedad (%) -0.257390 Presión (hPa) -0.462783 Calefacción (kWh) -0.530983 Name: Energía total (kWh), dtype: float64
Como se puede ver, aparte de "Día de la semana", todas las variables aportan al dataset, y como hemos dicho varias veces, no eliminamos "Día de la semana" porque es una variable exógena que nos aportará información a los modelos cuando se entrenen sobre el dataset.
Por último, vamos a revisar la matriz de correlación.
# Crear la tabla de correlación
correlation_matrix = energia_climaticos.corr()
plt.figure(figsize=(10,8))
sns.heatmap(correlation_matrix, annot=True, fmt=".2f", cmap="coolwarm")
plt.title("Matriz de correlación")
plt.show()
A diferencia de la última vez, en la que "Emisión (kg CO₂)" no la eliminábamos porque teníamos muy pocas variables, como ahora tenemos 13, aquí decidimos eliminarla y así no tratamos con el riesgo de multicolinealidad.
energia_climaticos = energia_climaticos.drop('Emisión (kg CO₂)', axis=1)
energia_climaticos.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 78888 entries, 2014-01-01 00:00:00 to 2022-12-31 23:00:00 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Día de la semana 78888 non-null int64 1 Electricidad (kW) 78888 non-null float64 2 Fotovoltaica (kW) 78888 non-null float64 3 Refrigeración (kW) 78888 non-null float64 4 Calefacción (kWh) 78888 non-null float64 5 Energía total (kWh) 78888 non-null float64 6 Temperatura (°C) 78888 non-null float64 7 Punto de rocío (°C) 78888 non-null float64 8 Humedad (%) 78888 non-null float64 9 Velocidad del viento (m/s) 78888 non-null float64 10 Ráfaga de viento (m/s) 78888 non-null float64 11 Presión (hPa) 78888 non-null float64 dtypes: float64(11), int64(1) memory usage: 9.8 MB
Transformacion a días¶
Ante la gran cantidad de datos y que nuestro dataset no tiene mucho sentido que tenga los datos en formato horario, decidimos cambiarlo a que sean diarios. Como nos piden un horizonte temporal de 7 días, cambiamos la granularidad de los datos. Para ello, hacemos un resample diario que sume todos los datos de cada dia, pero teniendo en cuenta que "Día de la semana" no queremos que cambie, y como cada fila es la suma de si mismo 24 veces, decidimos hacer una división de 24 y transformarlo en entero. Para agregar los datos, usamos la media diaria para las columnas Temperatura, Punto de rocío, Humedad, Velocidad del viento y Presión. Para la variable Ráfaga de viento, usamos el máximo de ese día.
# Guardamos una copia
energia_climaticos_horas = energia_climaticos
# Aquí pondremos todas las variables que les haremos un resample a través de la suma para transformarlas a diarias
columnas_suma = ['Día de la semana','Electricidad (kW)', 'Fotovoltaica (kW)', 'Refrigeración (kW)', 'Calefacción (kWh)', 'Energía total (kWh)']
# Aquí pondremos todas las variables que les haremos un resample a través de la media para transformarlas a diarias
columnas_media = ['Temperatura (°C)', 'Punto de rocío (°C)', 'Humedad (%)', 'Velocidad del viento (m/s)', 'Presión (hPa)']
# Aquí pondremos todas las variables que les haremos un resample a través del máximo para transformarlas a diarias
columnas_maximo = ['Ráfaga de viento (m/s)']
# Cambia frecuencia temporal a diaria a través de la suma
energia_climaticos_suma = energia_climaticos[columnas_suma].resample('D').sum()
# Cambia frecuencia temporal a diaria a través de la suma
energia_climaticos_media = energia_climaticos[columnas_media].resample('D').mean()
# Cambia frecuencia temporal a diaria a través de la suma
energia_climaticos_maximo = energia_climaticos[columnas_maximo].resample('D').max()
# Unimos los 3 dataframes con frecuencia diaria, unidos a través del índice temporal
energia_climaticos = energia_climaticos_suma.join(energia_climaticos_media).join(energia_climaticos_maximo)
# Divide entre 24 los datos de la variable
energia_climaticos['Día de la semana'] = energia_climaticos['Día de la semana']/24
# Transforma a entero la variable
energia_climaticos['Día de la semana'] = energia_climaticos['Día de la semana'].astype(int)
Revisamos cómo han quedado las 10 primeras filas.
# Visualizamos las 5 primeras columnas del dataset
energia_climaticos.head(10)
| Día de la semana | Electricidad (kW) | Fotovoltaica (kW) | Refrigeración (kW) | Calefacción (kWh) | Energía total (kWh) | Temperatura (°C) | Punto de rocío (°C) | Humedad (%) | Velocidad del viento (m/s) | Presión (hPa) | Ráfaga de viento (m/s) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2014-01-01 | 2 | 490049.28 | 62795.47 | 243369.71770 | 20107.60131 | 753526.59901 | 13.402778 | -6.990741 | 41.625000 | 1.434253 | 977.918882 | 0.0 |
| 2014-01-02 | 3 | 556486.56 | 64284.00 | 323886.05169 | 23987.86135 | 904360.47304 | 15.023148 | -2.013889 | 38.750000 | 1.490133 | 977.580243 | 0.0 |
| 2014-01-03 | 4 | 545831.45 | 67187.06 | 337004.60237 | 22112.20695 | 904948.25932 | 15.763889 | -3.356481 | 33.875000 | 1.080347 | 973.558905 | 0.0 |
| 2014-01-04 | 5 | 511495.56 | 46798.28 | 252493.06189 | 21074.73561 | 785063.35750 | 13.495370 | 1.944444 | 47.375000 | 1.862667 | 973.008617 | 0.0 |
| 2014-01-05 | 6 | 502806.80 | 70162.87 | 249265.37031 | 22777.47812 | 774849.64843 | 11.990741 | -4.328704 | 37.750000 | 1.043093 | 978.328071 | 0.0 |
| 2014-01-06 | 0 | 543968.35 | 71666.60 | 285299.00483 | 27314.21720 | 856581.57203 | 12.268519 | -11.134259 | 23.541667 | 1.713653 | 981.502812 | 0.0 |
| 2014-01-07 | 1 | 562002.91 | 52517.04 | 274781.34599 | 23633.24544 | 860417.50143 | 13.402778 | -7.500000 | 25.875000 | 1.378373 | 977.552023 | 0.0 |
| 2014-01-08 | 2 | 557214.72 | 66145.06 | 265609.15067 | 24020.09916 | 846843.96983 | 13.263889 | -4.236111 | 32.500000 | 1.564640 | 976.056368 | 0.0 |
| 2014-01-09 | 3 | 563161.10 | 50888.52 | 243592.34380 | 23108.64835 | 829862.09215 | 12.430556 | -3.495370 | 35.125000 | 1.769533 | 975.118055 | 0.0 |
| 2014-01-10 | 4 | 520356.53 | 62773.62 | 235918.63667 | 22238.22748 | 778513.39415 | 12.106481 | -2.337963 | 39.125000 | 1.825413 | 976.874745 | 0.0 |
Revisamos cómo ha quedado nuestro dataset en general.
# Información del dataset
energia_climaticos.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 3287 entries, 2014-01-01 to 2022-12-31 Freq: D Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Día de la semana 3287 non-null int64 1 Electricidad (kW) 3287 non-null float64 2 Fotovoltaica (kW) 3287 non-null float64 3 Refrigeración (kW) 3287 non-null float64 4 Calefacción (kWh) 3287 non-null float64 5 Energía total (kWh) 3287 non-null float64 6 Temperatura (°C) 3287 non-null float64 7 Punto de rocío (°C) 3287 non-null float64 8 Humedad (%) 3287 non-null float64 9 Velocidad del viento (m/s) 3287 non-null float64 10 Presión (hPa) 3287 non-null float64 11 Ráfaga de viento (m/s) 3287 non-null float64 dtypes: float64(11), int64(1) memory usage: 333.8 KB
Vemos cómo nuestro dataset ha pasado de 78888 a 3287 entradas (78888/24) y todo lo demás queda igual.
Análisis de las variables¶
Hay que tener en cuenta que aunque en varias partes de los análisis de las variables tengamos que ir cambiando su granularidad (para que se vean más suavizadas las lineas temporales de las variables y podamos interpretarlas com mayor sencillez), siempre tendremos frecuencia diaria en nuestro dataset, y esto no cambiará.
Vamos a analizar las variables climáticas que hemos añadido a nuestro dataset.
Temperatura (°C)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia_climaticos['Temperatura (°C)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 13 y 37, haciendo que la mayor parte de Temperatura (°C) esté entre la parte superior derecha y mediana inferior del histograma, haciendo que haya una cola hacia la izquierda y por lo tanto, un sesgo negativo, que implica que no es una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a revisar el resto de componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia_climaticos['Temperatura (°C)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que no parece haber una tendencia clara. Parece haber estacionalidad anual y el ruido parace tener variabilidad constante y estar centrado en 0. Parece que en esta serie también hay bastantes datos ruidosos.
Con periodo anual podemos ver claramente la tendencia, estacionalidad y ruido de la serie temporal de la variable.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').mean()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
ax.set_ylim(23, 26)
plt.show()
Con granularidad anual a partir de la media, podemos ver cómo hay una descendencia desde 2014 hasta 2016, hay un aumento ligero hasta 2017, luego un descenso hasta 2019, asciende de nuevo hasta 2020, baja hasta 2021, y luego asciende.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver como hay estacionalidad de forma clara, por lo que seguramente se repetirá el patrón en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, en cuanto al ruido, ronda alrededor de 0, no parece haber estacionalidad, ni outliers ni tendencia, y al principio de la gráfica hay más ruido que al final.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia_climaticos['Temperatura (°C)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -3.384562 p-value: 0.011497 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El p-valor es 0.011497 < 0.05, por lo que se rechaza la hipótesis nula, por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Temperatura (°C)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
La autocorrelación se mantiene por encima del 75% haciendo que haya dependencia entre valores pasados y presentes. Por tanto, parece que cada dato esta correlacionado con los datos de los 7 días anteriores.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Temperatura (°C)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que dentro de un mes (30 días), la autocorrelación sigue siendo bastante alta. A partir de la mitad del año (180 días aprox.), existe una autorrelación negativa destacable. Esto puede confirmarnos, tal y como se puede ver en las gráficas, que el valor de la serie durante la mitad del año baja con la misma fuerza con la que subió con durante la otra mitad. Con un año de lag (365 días), confirmamos también que hay mucha autocorrelación y por tanto, que hay estacionalidad anual.
Punto de rocío (°C)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia_climaticos['Punto de rocío (°C)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores -8 y 18, haciendo que la mayor parte de Punto de rocío (°C) esté entre la parte mediana baja y superior del histograma, haciendo que haya una cola hacia la izquierda y por lo tanto, un sesgo negativo, que implica que no llega a ser una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a revisar el resto de componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia_climaticos['Punto de rocío (°C)'], model='additive', period=365)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que no parece haber una tendencia clara de los datos hacia el futuro. Parece haber estacionalidad anual y el ruido parace tener variabilidad constante y estar centrado en 0. Parece que en esta serie también hay bastantes datos ruidosos.
Con periodo anual podemos ver claramente la tendencia, estacionalidad y ruido de la serie temporal de la variable.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').mean()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad anual y la media, vemos en la tendencia un ascenso desde 2014 a 2015, desciende hasta el 2017, asciende hasta 2019, desciende hasta 2020 y sube ligeramente hasta 2022.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver cómo hay estacionalidad de forma clara, por lo que seguramente se repetirá el patrón en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, en cuanto al ruido, ronda alrededor de 0, no parece haber estacionalidad, ni outliers ni tendencia, y parece haber más ruido en el centro que en los extremos.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia_climaticos['Punto de rocío (°C)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -4.817398 p-value: 0.000050 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El p-valor es 0.000050 < 0.05, por lo que se rechaza la hipótesis nula, por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Punto de rocío (°C)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
La autocorrelación se mantiene por encima del 50% haciendo que haya dependencia entre valores pasados y presentes. Por lo que hay bastante correlación de cada datos con los de la semana anterior.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Punto de rocío (°C)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que dentro de un mes (30 días), la autocorrelación es significativa. A partir de la mitad del año (180 días aprox.), existe una autorrelación negativa apreciable. Con un año de lag (365 días), confirmamos también que hay cierta autocorrelación y por tanto, que hay estacionalidad anual.
Humedad (%)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia_climaticos['Humedad (%)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 10 y 50, haciendo que la mayor parte de Humedad (%) esté entre la parte inferior y mediana inferior del histograma, haciendo que haya una cola hacia la derecha y por lo tanto, un sesgo positivo, que implica que no es una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a revisar el resto de componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia_climaticos['Humedad (%)'], model='additive', period=365)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que no parece haber una tendencia clara. Parece haber estacionalidad anual y el ruido parace tener variabilidad constante y estar centrado en 0. Parece que en esta serie también hay bastantes datos ruidosos.
Con periodo anual podemos ver claramente la tendencia, estacionalidad y ruido de la serie temporal de la variable.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').mean()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
ax.set_ylim(25, 35)
plt.show()
Con granularidad anual, vemos en la tendencia un ascenso desde 2014 a 2015, desciende hasta el 2017, asciende hasta 2019, desciende hasta 2020 y sube hasta 2022.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver cómo hay estacionalidad de forma clara, por lo que seguramente se repetirá el patrón en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, en cuanto al ruido, ronda alrededor de 0, no parece haber estacionalidad, ni outliers ni tendencia, y en el centro de la serie parace haber más ruido que en los extremos de la misma.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia_climaticos['Humedad (%)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -4.928611 p-value: 0.000031 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El p-valor es 0.000031 < 0.05, por lo que se rechaza la hipótesis nula. Por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Humedad (%)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
La autocorrelación se mantiene por encima del 50% haciendo que haya dependencia entre valores pasados y presentes. Existe una correlación de cada dato con los de los 7 días anteriores.
# Se crea una gráfica de tamaño (10, 6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Humedad (%)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que dentro de un mes (30 días), la autocorrelación sigue siendo apreciable. A partir de la mitad del año (180 días aprox.), existe una cierta autorrelación negativa. Con un año de lag (365 días), confirmamos también que hay alguna autocorrelación y por tanto, que hay estacionalidad anual.
Velocidad del viento (m/s)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia_climaticos['Velocidad del viento (m/s)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 1 y 4.5, haciendo que la mayor parte de Velocidad del viento (m/s) esté entre la parte inferior y mediana inferior del histograma, haciendo que haya una cola hacia la derecha y por lo tanto, un sesgo positivo, que implica que no es una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a revisar el resto de componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia_climaticos['Velocidad del viento (m/s)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que no parece haber una tendencia clara hacia el futuro. Parece haber estacionalidad anual y el ruido parace tener variabilidad constante, poca desviación típica y estar centrado en 0. Parece que en esta serie también hay bastantes datos ruidosos.
Con periodo anual podemos ver claramente la tendencia, estacionalidad y ruido de la serie temporal de la variable.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').mean()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
ax.set_ylim(2.8, 3.0)
plt.show()
Con granularidad anual y la media, podemos ver como hay una ascendencia desde 2014 hasta 2016, desciende hasta 2018, asciende hasta 2019, desciende hasta 2020 y aumenta hasta 2022.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver como hay estacionalidad de forma clara, por lo que seguramente se repetirá el patrón en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, en cuanto al ruido, ronda alrededor de 0, no parece haber estacionalidad, ni outliers ni tendencia, y al en el medio de la gráfica hay más ruido que en los extremos.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia_climaticos['Velocidad del viento (m/s)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -4.373794 p-value: 0.000330 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El p-valor es 0.000330 < 0.05, por lo que se rechaza la hipótesis nula. Por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Velocidad del viento (m/s)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
La correlación se puede ver como en los lags 2 (40-45%) y 3 (20-25%) parece haber un bajón en la correlación, aunque luego esta última, se mantiene hasta la 7, por lo que la dependencia entre valores pasados y presentes es bastante floja, a diferencia del resto de variables que hemos ido mirando durante el notebook. No parece haber mucha correlación de cada dato con los de la semana anterior.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Velocidad del viento (m/s)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que dentro de un mes (30 días), la autocorrelación sigue siendo bastante floja. A partir de la mitad del año (180 días aprox.), existe una autorrelación negativa pequeña. Con un año de lag (365 días), confirmamos también que hay poca autocorrelación.
Presión (hPa)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia_climaticos['Presión (hPa)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede ver cómo gran parte de los datos están situados entre los valores 965 y 978, haciendo que la mayor parte de Presión (hPa) esté entre la parte inferior y mediana superior del histograma, haciendo que haya una cola hacia la derecha y por lo tanto, un sesgo positivo, que implica que no es una distribución normal. Vemos también cómo la distribución de los datos parece ser bastante ancha.
Ahora vamos a revisar el resto de componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia_climaticos['Presión (hPa)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que no parece haber una tendencia clara. Parece haber estacionalidad anual y el ruido parace tener variabilidad constante, tener poca desviación típica y estar centrado en 0. Parece que en esta serie también hay bastantes datos ruidosos.
Con periodo anual podemos ver claramente la tendencia, estacionalidad y ruido de la serie temporal de la variable.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').mean()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
ax.set_ylim(972, 973)
plt.show()
Con granularidad anual y media, podemos ver un descenso desde 2014 hasta 2015, asciende hasta 2016, desciende hasta 2017, asciende hasta 2018, desciende hasta 2020, y asciende hasta 2022.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver como hay estacionalidad de forma clara, por lo que seguramente se repetirá el patrón en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, en cuanto al ruido, ronda alrededor de 0, no parece haber estacionalidad, ni outliers ni tendencia, y al principio de la gráfica hay menos ruido que al final.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia_climaticos['Presión (hPa)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -4.657650 p-value: 0.000101 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El p-valor es 0.000101 < 0.05, por lo que se rechaza la hipótesis nula. Por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Presión (hPa)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Se puede ver cómo hay una autocorrelación por encima del 0.5 para los 7 lags. Existe una cierta autocorrelación entre cada dato y los de los 7 días anteriores.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Presión (hPa)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que dentro de un mes (30 días), la autocorrelación sigue siendo notable. A partir de la mitad del año (180 días aprox.), existe una cierta autorrelación negativa. Esto puede confirmarnos, tal y como se puede ver en las gráficas, que el valor de la serie durante la mitad del año baja con la misma fuerza con la que subió con durante la otra mitad. Con un año de lag (365 días), confirmamos también que hay una cierta autocorrelación y por tanto, que hay estacionalidad anual.
Ráfaga de viento (m/s)¶
Realizamos el histograma para ver cómo están distribuidos los datos.
# Creamos un histograma de tamaño (8,4) dividiéndolo en 60 intervalos
energia_climaticos['Ráfaga de viento (m/s)'].hist(bins=60, figsize=(8,4))
plt.show()
Se puede observar, a diferencia del resto de variables analizadas hasta ahora, que una gran parte de los valores se concentra en 0, ya que este valor se corresponde a su comportamiento habitual. A partir del intervalo entre 5 y 10 m/s comienzan a aparecer datos, disminuyendo progresivamente su frecuencia en los tramos de 10 a 15 m/s y de 15 a 20 m/s, siendo prácticamente inexistentes en el intervalo entre 20 y 25 m/s.
Esta distribución presenta una cola hacia la derecha, lo que indica un sesgo positivo acusado y, por tanto, una clara desviación respecto a una distribución normal. Este comportamiento es característico de variables asociadas a eventos poco frecuentes pero de elevada intensidad, como son las ráfagas de viento puntuales en un día.
Ahora vamos a revisar el resto de componentes de la variable.
# Descompone la serie temporal de la variable en sus 3 componentes principales: tendencia, estacionalidad y ruido
resultado = seasonal_decompose(energia_climaticos['Ráfaga de viento (m/s)'], model='additive', period=365)
# Se imprime con un tamaño (16,12)
fig = resultado.plot()
fig.set_size_inches((16,12))
plt.show()
Vemos que parece haber una tendencia claramente descendente a partir del final de la serie. Parece haber estacionalidad anual y el ruido parace tener variabilidad constante y estar centrado en 0. Parece que en esta serie también hay bastantes datos ruidosos.
Con periodo anual podemos ver claramente la tendencia, estacionalidad y ruido de la serie temporal de la variable.
# Reduce granularidad a años, sumando los valores
trend = resultado.trend.resample('YE').max()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
trend.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
ax.set_ylim(5, 8)
plt.show()
Con granularidad anual, podemos ver como hay una ascendencia desde 2014 hasta 2016, descenso hasta 2017, ascenso hasta 2018, se mantiene hasta 2019, desciende hasta 2020 y asciende enormemente hasta 2022.
# Reduce granularidad a meses, sumando los valores
seasonal = resultado.seasonal.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
seasonal.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual podemos ver cómo hay estacionalidad de forma clara, por lo que seguramente se repetirá el patrón en el futuro.
# Reduce granularidad a meses, sumando los valores
resid = resultado.resid.resample('ME').sum()
# Imprimimos con un tamaño (16,4)
fig, ax = plt.subplots(figsize=(16,4))
resid.plot(ax=ax, color='blue', linewidth=1)
ax.axhline(0, color='black', linestyle='--')
ax.grid(True)
plt.show()
Con granularidad mensual, en cuanto al ruido, ronda alrededor de 0, no parece haber estacionalidad, ni outliers ni tendencia, y parece estar igualmente distribuido a lo largo de la serie.
# Se aplica el test ADF (Augmented Dickey-Fuller) para comprobar la estacionariedad de la serie temporal de la variable
result = adfuller(energia_climaticos['Ráfaga de viento (m/s)'])
print('Estadístico ADF : %f' % result[0])
print('p-value: %f' % result[1])
print('Valores críticos:')
for key, value in result[4].items():
print('\t%s: %.3f' % (key, value))
Estadístico ADF : -4.157892 p-value: 0.000776 Valores críticos: 1%: -3.432 5%: -2.862 10%: -2.567
El p-valor es 0.000776 < 0.05, por lo que se rechaza la hipótesis nula. Por lo tanto es estacionaria, es decir, la varianza y la media son constantes y la estructura temporal se mantiene.
Por lo tanto decidimos no transformar nuestra serie temporal.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Ráfaga de viento (m/s)'], lags=7)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
En la autocorrelación se puede ver como en el lag 2 hay un decremento enorme del 100% a 30-35%, en el lag 3 baja hasta 25-30%, y luego se mantiene en los siguientes lags, lo que indica una dependencia entre valores pasados y presentes muy floja. Como las ráfagas de viento se producen irregularmente a lo largo de los días, hay poca autocorrelación entre cada dato y los 7 anteriores.
# Se crea una gráfica de tamaño (10,6)
plt.figure(figsize=(10, 6))
# Se representa la función de autocorrelación (ACF) de la serie temporal de temperatura dependiente de los lags establecidos
sm.graphics.tsa.plot_acf(energia_climaticos['Ráfaga de viento (m/s)'], lags=365)
plt.title('Función de Autocorrelación (ACF)')
plt.xlabel('Lags')
plt.ylabel('Autocorrelación')
plt.show()
<Figure size 1000x600 with 0 Axes>
Vemos que dentro de un mes (30 días), la autocorrelación sigue siendo bastante floja. A partir de la mitad del año (180 días aprox.), esta sigue siendo floja. Con un año de lag (365 días), confirmamos también que hay poca autocorrelación.
Variables endógenas y exógenas¶
En vista al análisis, en cuanto a las variables endógenas, variables que influyen sobre la variable objetivo, son las variables: Electricidad (kW), Fotovoltaica (kW), Refrigeración (kW) y Calefacción (kWh).
En cuanto a las variables exógenas, tenemos: Día de la semana, Temperatura (°C), Punto de rocío (°C), Humedad (%), Presión (hPa), Velocidad del viento (m/s) y Ráfaga de viento (m/s). Incorporamos todas las variables meteorológicas como variables exógenas (humedad, presión atmosférica, velocidad del viento y ráfaga), ya que son factores externos al sistema eléctrico que influyen en la demanda a través del confort térmico, las pérdidas/ganancias de calor y, en general, de la necesidad de calefacción o refrigeración. Además, a diferencia del consumo energético (que es la variable objetivo), estas variables se pueden estimar para el futuro mediante modelos meteorológicos robustos, lo que permite disponer de valores exógenos en el horizonte de predicción e introducirlos en los modelos sin incurrir en fuga de información.
Guardar dataset¶
Guardamos el dataset con todos los datos ya preprocesados.
# energia_climaticos.head()
# energia_climaticos.to_csv('Data/energia_climaticos_preprocesada.csv')
Uso de IA¶
Prompt: pero pierde puntos del eje x, como lo hago para poner todos los meses?
Respuesta:
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
# Selección de rango
ts = energia_mensual["2014":"2015"]
# Crear la figura
fig, ax = plt.subplots(figsize=(12,4))
# Graficar línea principal
ax.plot(ts.index, ts.values, marker='o', color='blue', label='Consumo mensual')
# Resaltar el pico de cada año
for year in ts.index.year.unique():
pico = ts[ts.index.year == year].idxmax()
ax.scatter(pico, ts[pico], color='red', zorder=5)
ax.text(pico, ts[pico]+0.1, str(pico.month), ha='center', color='red')
# Título y etiquetas
ax.set_title("Consumo medio mensual (2014-2015) con picos estacionales")
ax.set_xlabel("Mes")
ax.set_ylabel("Electricidad (kW)")
# Grid
ax.grid(color='gray', linestyle='--', linewidth=0.7)
# Configurar eje X para mostrar todos los meses
ax.xaxis.set_major_locator(mdates.MonthLocator()) # cada mes
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
# Rotar etiquetas para que no se superpongan
plt.xticks(rotation=45)
# Leyenda
ax.legend()
plt.tight_layout()
plt.show()
Prompt: hazme una tabla de correlacion de todas las columnas de un conjunto de datos en python
Respuesta:
import pandas as pd
# Cargar tu conjunto de datos
# Por ejemplo, si es un CSV:
df = pd.read_csv("ruta/a/tu/archivo.csv")
# Crear la tabla de correlación
correlation_matrix = df.corr()
# Mostrar la tabla de correlación
print(correlation_matrix)
# Opcional: si quieres visualizarla de forma más atractiva con un heatmap
import seaborn as sns
import matplotlib.pyplot as plt
plt.figure(figsize=(10,8))
sns.heatmap(correlation_matrix, annot=True, fmt=".2f", cmap="coolwarm")
plt.title("Matriz de correlación")
plt.show()
Prompt: Hazme en una grafica donde se muestren las series temporales de todas las variables de mi dataset energia
Respuesta:
# Guarda el número de variables del dataset
n = len(energia.columns)
# Creamos las figuras y ejes con 1 columna que cada fila tendrá una variable, en figsize poniendo anchura y altura, comparten el mismo eje x
fig, axes = plt.subplots(nrows=n, ncols=1, figsize=(14, max(2, 1.6 * n)), sharex=True)
# Dibuja la serie temporal de cada columna, pone su nombre al eje y y coloca un grid
for ax, col in zip(axes, energia.columns):
ax.plot(energia.index, energia[col], linewidth=1)
ax.set_ylabel(col)
ax.grid(True, linestyle='--', linewidth=0.5, alpha=0.5)
# Coloca en la última gráfica el eje X
axes[-1].set_xlabel("Tiempo")
# Nombre general
fig.suptitle("Series temporales del barrio — cada serie en su propia fila", y=0.995)
# Distancia entre filas
plt.tight_layout(rect=[0, 0, 1, 0.99])
# Se muestra la gráfica
plt.show()
Prompt: me sale raro, yo lo que quiero es que se vea la serie temporal de la columna seleccionada como su tuviese zoom apuntando a donde estan los nulos y se vea el hueco, y luego otra grafica igual pero ese hueco rellenado por la linea de la interpolacion temporal pintado de otro color
Respuesta:
col = 'Energia'
# índices donde hay NaN
idx_nan = energia_original[col][energia_original[col].isna()].index
# elegimos el primer hueco
t0 = idx_nan[0]
# ventana de zoom (ej. ±12 horas)
inicio = t0 - pd.Timedelta(hours=12)
fin = t0 + pd.Timedelta(hours=12)
orig_zoom = energia_original.loc[inicio:fin, col]
interp_zoom = energia.loc[inicio:fin, col]
mask = orig_zoom.isna()
import matplotlib.pyplot as plt
plt.figure(figsize=(10,4))
plt.plot(orig_zoom.index, orig_zoom, marker='o')
plt.title('Serie temporal con hueco (valores nulos)')
plt.xlabel('Tiempo')
plt.ylabel(col)
plt.tight_layout()
plt.show()
# Copia para dibujar
serie_azul = interp_zoom.copy()
# En el hueco interpolado, ponemos NaN para que NO se dibuje azul
serie_azul[mask] = float('nan')
plt.figure(figsize=(10,4))
# Azul: datos originales (con corte)
plt.plot(
serie_azul.index,
serie_azul,
color='steelblue',
label='Datos originales'
)
# Naranja: tramo interpolado + bordes
idx_interp = interp_zoom.index[mask]
# añadimos un punto antes y después para continuidad visual
idx_ext = interp_zoom.index[
interp_zoom.index.get_loc(idx_interp[0]) - 1 :
interp_zoom.index.get_loc(idx_interp[-1]) + 2
]
plt.plot(
idx_ext,
interp_zoom.loc[idx_ext],
color='orange',
linewidth=3,
label='Tramo interpolado'
)
plt.title('Serie temporal con hueco rellenado por interpolación')
plt.xlabel('Tiempo')
plt.ylabel(col)
plt.legend()
plt.tight_layout()
plt.show()
Librerías utlizadas¶
import session_info
# En versiones más recientes de python (3.12.7), este es el comando
session_info.show()
Click to view session information
----- session_info v1.0.1 -----
Click to view modules imported as dependencies
asttokens NA colorama 0.4.6 comm 0.2.3 cython_runtime NA dateutil 2.9.0.post0 debugpy 1.8.17 decorator 5.2.1 executing 2.2.1 ipykernel 7.1.0 jedi 0.19.2 packaging 25.0 parso 0.8.5 platformdirs 4.5.0 prompt_toolkit 3.0.52 psutil 7.1.3 pure_eval 0.2.3 pydev_ipython NA pydevconsole NA pydevd 3.2.3 pydevd_file_utils NA pydevd_plugins NA pydevd_tracing NA pygments 2.19.2 six 1.17.0 stack_data 0.6.3 tornado 6.5.2 traitlets 5.14.3 vscode NA wcwidth 0.2.14 zmq 27.1.0
----- IPython 9.7.0 jupyter_client 8.6.3 jupyter_core 5.9.1 ----- Python 3.12.7 (tags/v3.12.7:0b05ead, Oct 1 2024, 03:06:41) [MSC v.1941 64 bit (AMD64)] Windows-10-10.0.19045-SP0 ----- Session information updated at 2026-01-08 06:24